Skip to content
config_translation.contextual_links:
title: 'Translate @type_name'
derivative: 'Drupal\config_translation\Plugin\Derivative\ConfigTranslationContextualLinks'
weight: 100
name: 'Configuration Translation'
type: module
description: 'Provides a translation interface for configuration.'
package: Multilingual
version: VERSION
core: 8.x
dependencies:
- locale
config_translation.local_tasks:
title: 'Translate @type_name'
derivative: 'Drupal\config_translation\Plugin\Derivative\ConfigTranslationLocalTasks'
weight: 100
<?php
/**
* @file
* Configuration Translation module.
*/
use Drupal\config_translation\Plugin\Derivative\ConfigTranslationLocalTasks;
use Drupal\Core\Entity\EntityInterface;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
/**
* Implements hook_help().
*/
function config_translation_help($path) {
switch ($path) {
case 'admin/help#config_translation':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Configuration Translation module allows configurations to be translated into different languages. Views, your site name, contact module categories, vocabularies, menus, blocks, and so on are all stored within the unified configuration system and can be translated with this module. Content, such as nodes, taxonomy terms, custom blocks, and so on are translatable with the Content Translation module in Drupal core, while the built-in user interface (such as registration forms, content submission and administration interfaces) are translated with the Interface Translation module. Use these three modules effectively together to translate your whole site to different languages.') . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dl>';
$output .= '<dt>' . t('Translating') . '</dt>';
$output .= '<dd>' . t('To translate configuration items, select the translate tab when viewing the configuration, select the language for which you wish to provide translations and then enter the content.') . '</dd>';
$output .= '</dl>';
return $output;
case 'admin/config/regional/config-translation':
$output = '<p>' . t('This page lists all configuration items on your site which have translatable text, like your site name, role names, etc.') . '</p>';
return $output;
}
}
/**
* Implements hook_menu().
*/
function config_translation_menu() {
$items = array();
$items['admin/config/regional/config-translation'] = array(
'title' => 'Configuration translation',
'description' => 'Translate the configuration.',
'route_name' => 'config_translation.mapper_list',
'weight' => 30,
);
return $items;
}
/**
* Implements hook_permission().
*/
function config_translation_permission() {
return array(
'translate configuration' => array(
'title' => t('Translate user edited configuration'),
'description' => t('Translate any configuration not shipped with modules and themes.'),
),
);
}
/**
* Implements hook_theme().
*/
function config_translation_theme() {
return array(
'config_translation_manage_form_element' => array(
'render element' => 'element',
'template' => 'config_translation_manage_form_element',
),
);
}
/**
* Implements hook_config_translation_info().
*/
function config_translation_config_translation_info(&$info) {
$entity_manager = \Drupal::entityManager();
$route_provider = \Drupal::service('router.route_provider');
// If field UI is not enabled, the base routes of the type
// "field_ui.instance_edit_$entity_type" are not defined.
if (\Drupal::moduleHandler()->moduleExists('field_ui')) {
// Add fields entity mappers to all fieldable entity types defined.
foreach ($entity_manager->getDefinitions() as $entity_type => $entity_info) {
$base_route = NULL;
try {
$base_route = $route_provider->getRouteByName('field_ui.instance_edit_' . $entity_type);
}
catch (RouteNotFoundException $e) {
// Ignore non-existent routes.
}
// Make sure entity type is fieldable and has a base route.
if ($entity_info['fieldable'] && !empty($base_route)) {
$info[$entity_type . '_fields'] = array(
'base_route_name' => 'field_ui.instance_edit_' . $entity_type,
'entity_type' => 'field_instance',
'title' => '!label field',
'class' => '\Drupal\config_translation\ConfigFieldInstanceMapper',
'base_entity_type' => $entity_type,
'list_controller' => '\Drupal\config_translation\Controller\ConfigTranslationFieldInstanceListController',
'weight' => 10,
);
}
}
}
// Discover configuration entities automatically.
foreach ($entity_manager->getDefinitions() as $entity_type => $entity_info) {
// Determine base path for entities automatically if provided via the
// configuration entity.
if (
!in_array('Drupal\Core\Config\Entity\ConfigEntityInterface', class_implements($entity_info['class'])) ||
!isset($entity_info['links']['edit-form'])
) {
// Do not record this entity mapper if the entity type does not
// provide a base route. We'll surely not be able to do anything with
// it anyway. Configuration entities with a dynamic base path, such as
// field instances, need special treatment. See above.
continue;
}
// Use the entity type as the plugin ID.
$info[$entity_type] = array(
'class' => '\Drupal\config_translation\ConfigEntityMapper',
'base_route_name' => $entity_info['links']['edit-form'],
'title' => '!label !entity_type',
'names' => array(),
'entity_type' => $entity_type,
'weight' => 10,
);
if ($entity_type == 'block') {
// Blocks placements need a specific list controller.
$info['block']['list_controller'] = '\Drupal\config_translation\Controller\ConfigTranslationBlockListController';
}
}
}
/**
* Implements hook_entity_operation_alter().
*/
function config_translation_entity_operation_alter(array &$operations, EntityInterface $entity) {
if (\Drupal::currentUser()->hasPermission('translate configuration')) {
$uri = $entity->uri();
$operations['translate'] = array(
'title' => t('Translate'),
'href' => $uri['path'] . '/translate',
'options' => $uri['options'],
'weight' => 50,
);
}
}
/**
* Implements hook_config_translation_type_info_alter().
*/
function config_translation_config_translation_type_info_alter(&$definitions) {
// Enhance the text and date type definitions with classes to generate proper
// form elements in ConfigTranslationFormBase. Other translatable types will
// appear as a one line textfield.
$definitions['text']['form_element_class'] = '\Drupal\config_translation\FormElement\Textarea';
$definitions['date_format']['form_element_class'] = '\Drupal\config_translation\FormElement\DateFormat';
}
/**
* Implements hook_library_info().
*/
function config_translation_library_info() {
$libraries['drupal.config_translation.admin'] = array(
'title' => 'Configuration translation admin',
'version' => \Drupal::VERSION,
'css' => array(
drupal_get_path('module', 'config_translation') . '/css/config_translation.admin.css' => array(),
),
);
return $libraries;
}
/**
* Implements hook_local_tasks_alter().
*/
function config_translation_local_tasks_alter(&$local_tasks) {
// Alters in tab_root_ids onto the config translation local tasks.
$derivative = ConfigTranslationLocalTasks::create(\Drupal::getContainer(), 'config_translation.local_tasks');
$derivative->alterLocalTasks($local_tasks);
}
config_translation.mapper_list:
path: '/admin/config/regional/config-translation'
defaults:
_title: 'Configuration translation'
_content: '\Drupal\config_translation\Controller\ConfigTranslationMapperList::render'
requirements:
_permission: 'translate configuration'
config_translation.entity_list:
path: '/admin/config/regional/config-translation/{config_translation_mapper}'
defaults:
_content: '\Drupal\config_translation\Controller\ConfigTranslationListController::listing'
requirements:
_permission: 'translate configuration'
services:
config_translation.route_subscriber:
class: Drupal\config_translation\Routing\RouteSubscriber
arguments: ['@plugin.manager.config_translation.mapper']
tags:
- { name: event_subscriber }
config_translation.access.overview:
class: Drupal\config_translation\Access\ConfigTranslationOverviewAccess
arguments: ['@plugin.manager.config_translation.mapper']
tags:
- { name: access_check }
config_translation.access.form:
class: Drupal\config_translation\Access\ConfigTranslationFormAccess
arguments: ['@plugin.manager.config_translation.mapper']
tags:
- { name: access_check }
plugin.manager.config_translation.mapper:
class: Drupal\config_translation\ConfigMapperManager
arguments:
- '@cache.cache'
- '@language_manager'
- '@module_handler'
- '@config.typed'
/**
* @file
* Styles for configuration translation.
*/
/**
* Hide the label, in an accessible way, for responsive screens which show the
* form in one column.
*/
.config-translation-form .translation-element-wrapper .translation label {
position: absolute !important;
clip: rect(1px, 1px, 1px, 1px);
overflow: hidden;
height: 1px;
width: 1px;
}
/**
* For wider screens, show the label and display source and translation side by
* side.
*/
@media all and (min-width: 851px) {
.config-translation-form .translation-element-wrapper .source {
width: 48%;
float: left;
}
.config-translation-form .translation-element-wrapper .translation {
width: 48%;
float: right;
}
.config-translation-form .translation-element-wrapper .translation label {
position: static !important;
clip: auto;
overflow: visible;
height: auto;
width: auto;
}
}
<?php
/**
* @file
* Contains \Drupal\config_translation\Access\ConfigNameCheck.
*/
namespace Drupal\config_translation\Access;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
/**
* Checks access for displaying the translation add, edit, and delete forms.
*/
class ConfigTranslationFormAccess extends ConfigTranslationOverviewAccess {
/**
* {@inheritdoc}
*/
public function appliesTo() {
return array('_config_translation_form_access');
}
/**
* {@inheritdoc}
*/
public function access(Route $route, Request $request, AccountInterface $account) {
// For the translation forms we have a target language, so we need some
// checks in addition to the checks performed for the translation overview.
$base_access = parent::access($route, $request, $account);
if ($base_access === static::ALLOW) {
$target_language = language_load($request->attributes->get('langcode'));
// Make sure that the target language is not locked, and that the target
// language is not the original submission language. Although technically
// configuration can be overlaid with translations in the same language,
// that is logically not a good idea.
$access =
!empty($target_language) &&
!$target_language->locked &&
$target_language->id != $this->sourceLanguage->id;
return $access ? static::ALLOW : static::DENY;
}
return static::DENY;
}
}
<?php
/**
* @file
* Contains \Drupal\config_translation\Access\ConfigTranslationOverviewAccess.
*/
namespace Drupal\config_translation\Access;
use Drupal\config_translation\ConfigMapperManagerInterface;
use Drupal\Core\Access\StaticAccessCheckInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
/**
* Checks access for displaying the configuration translation overview.
*/
class ConfigTranslationOverviewAccess implements StaticAccessCheckInterface {
/**
* The mapper plugin discovery service.
*
* @var \Drupal\config_translation\ConfigMapperManagerInterface
*/
protected $configMapperManager;
/**
* The source language.
*
* @var \Drupal\Core\Language\Language
*/
protected $sourceLanguage;
/**
* Constructs a ConfigNameCheck object.
*
* @param \Drupal\config_translation\ConfigMapperManagerInterface $config_mapper_manager
* The mapper plugin discovery service.
*/
public function __construct(ConfigMapperManagerInterface $config_mapper_manager) {
$this->configMapperManager = $config_mapper_manager;
}
/**
* {@inheritdoc}
*/
public function appliesTo() {
return array('_config_translation_overview_access');
}
/**
* {@inheritdoc}
*/
public function access(Route $route, Request $request, AccountInterface $account) {
/** @var \Drupal\config_translation\ConfigMapperInterface $mapper */
$mapper = $this->configMapperManager->createInstance($route->getDefault('plugin_id'));
$mapper->populateFromRequest($request);
$this->sourceLanguage = $mapper->getLanguageWithFallback();
// Allow access to the translation overview if the proper permission is
// granted, the configuration has translatable pieces, and the source
// language is not locked.
$access =
$account->hasPermission('translate configuration') &&
$mapper->hasSchema() &&
$mapper->hasTranslatable() &&
!$this->sourceLanguage->locked;
return $access ? static::ALLOW : static::DENY;
}
}
<?php
/**
* @file
* Contains \Drupal\config_translation\ConfigEntityMapper.
*/
namespace Drupal\config_translation;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\ConfigFactory;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManager;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\locale\LocaleConfigManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Configuration mapper for configuration entities.
*/
class ConfigEntityMapper extends ConfigNamesMapper {
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManager
*/
protected $entityManager;
/**
* Configuration entity type name.
*
* @var string
*/
protected $entityType;
/**
* Loaded entity instance to help produce the translation interface.
*
* @var \Drupal\Core\Entity\EntityInterface
*/
protected $entity;
/**
* The label for the entity type.
*
* @var string
*/
protected $typeLabel;
/**
* Constructs a ConfigEntityMapper.
*
* @param string $plugin_id
* The config mapper plugin ID.
* @param array $plugin_definition
* An array of plugin information as documented in
* ConfigNamesMapper::__construct() with the following additional keys:
* - entity_type: The name of the entity type this mapper belongs to.
* @param \Drupal\Core\Config\ConfigFactory $config_factory
* The configuration factory.
* @param \Drupal\locale\LocaleConfigManager $locale_config_manager
* The locale configuration manager.
* @param \Drupal\config_translation\ConfigMapperManagerInterface $config_mapper_manager
* The mapper plugin discovery service.
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider.
* @param \Drupal\Core\StringTranslation\TranslationInterface $translation_manager
* The string translation manager.
* @param \Drupal\Core\Entity\EntityManager $entity_manager
* The entity manager.
*/
public function __construct($plugin_id, array $plugin_definition, ConfigFactory $config_factory, LocaleConfigManager $locale_config_manager, ConfigMapperManagerInterface $config_mapper_manager, RouteProviderInterface $route_provider, TranslationInterface $translation_manager, EntityManager $entity_manager) {
parent::__construct($plugin_id, $plugin_definition, $config_factory, $locale_config_manager, $config_mapper_manager, $route_provider, $translation_manager);
$this->setType($plugin_definition['entity_type']);
$this->entityManager = $entity_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) {
// Note that we ignore the plugin $configuration because mappers have
// nothing to configure in themselves.
return new static (
$plugin_id,
$plugin_definition,
$container->get('config.factory'),
$container->get('locale.config.typed'),
$container->get('plugin.manager.config_translation.mapper'),
$container->get('router.route_provider'),
$container->get('string_translation'),
$container->get('entity.manager')
);
}
/**
* {@inheritdoc}
*/
public function populateFromRequest(Request $request) {
parent::populateFromRequest($request);
$entity = $request->attributes->get($this->entityType);
$this->setEntity($entity);
}
/**
* 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 check permissions or display a translation
* screen.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to set.
*
* @return bool
* TRUE, if the entity was set successfully; FALSE otherwise.
*/
public function setEntity(EntityInterface $entity) {
if (isset($this->entity)) {
return FALSE;
}
$this->entity = $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 = $this->entityManager->getDefinition($this->entityType);
$this->addConfigName($entity_type_info['config_prefix'] . '.' . $entity->id());
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getTitle() {
// Title based on the entity label. Should be translated for display in the
// current page language. The title placeholder is later escaped for
// display.
$entity_type_info = $this->entityManager->getDefinition($this->entityType);
return $this->t($this->pluginDefinition['title'], array('!label' => $this->entity->label(), '!entity_type' => Unicode::strtolower($entity_type_info['label'])));
}
/**
* {@inheritdoc}
*/
public function getBaseRouteParameters() {
return array($this->entityType => $this->entity->id());
}
/**
* 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.
*
* @param string $entity_type
* The entity type to set.
*
* @return bool
* TRUE if the entity type was set correctly; FALSE otherwise.
*/
public function setType($entity_type) {
if (isset($this->entityType)) {
return FALSE;
}
$this->entityType = $entity_type;
return TRUE;
}
/**
* Gets the entity type from this mapper.
*
* @return string
*/
public function getType() {
return $this->entityType;
}
/**
* {@inheritdoc}
*/
public function getTypeName() {
$entity_type_info = $this->entityManager->getDefinition($this->entityType);
return $entity_type_info['label'];
}
/**
* {@inheritdoc}
*/
public function getTypeLabel() {
$entityType = $this->entityManager->getDefinition($this->entityType);
return $entityType['label'];
}
/**
* {@inheritdoc}
*/
public function getOperations() {
return array(
'list' => array(
'title' => $this->t('List'),
'href' => 'admin/config/regional/config-translation/' . $this->getPluginId(),
),
);
}
/**
* {@inheritdoc}
*/
public function getContextualLinkGroup() {
// @todo Contextual groups do not map to entity types in a predictable
// way. See https://drupal.org/node/2134841 to make them predictable.
switch ($this->entityType) {
case 'menu':
case 'block':
return $this->entityType;
case 'view':
return 'views_ui_edit';
default:
return NULL;
}
}
}
<?php
/**
* @file
* Contains \Drupal\config_translation\ConfigFieldInstanceMapper.
*/
namespace Drupal\config_translation;
/**
* Configuration mapper for field instances.
*
* On top of plugin definition values on ConfigEntityMapper, the plugin
* definition for field instance mappers are required to contain the following
* additional keys:
* - base_entity_type: The name of the entity type the field instances are
* attached to.
*/
class ConfigFieldInstanceMapper extends ConfigEntityMapper {
/**
* {@inheritdoc}
*/
public function getBaseRouteParameters() {
$parameters = parent::getBaseRouteParameters();
$base_entity_info = $this->entityManager->getDefinition($this->pluginDefinition['base_entity_type']);
// @todo Field instances have no method to return the bundle the instance is
// attached to. See https://drupal.org/node/2134861
$parameters[$base_entity_info['bundle_entity_type']] = $this->entity->bundle;
return $parameters;
}
/**
* {@inheritdoc}
*/
public function getTypeLabel() {
$base_entity_info = $this->entityManager->getDefinition($this->pluginDefinition['base_entity_type']);
return $this->t('@label fields', array('@label' => $base_entity_info['label']));
}
}
<?php
/**
* @file
* Contains \Drupal\config_translation\ConfigMapperInterface.
*/
namespace Drupal\config_translation;
use Drupal\Core\Language\Language;
use Symfony\Component\HttpFoundation\Request;
/**
* Defines an interface for configuration mapper.
*/
interface ConfigMapperInterface {
/**
* Returns title of this translation page.
*
* @return string
* The page title.
*/
public function getTitle();
/**
* Returns the name of the base route the mapper is attached to.
*
* @return string
* The name of the base route the mapper is attached to.
*/
public function getBaseRouteName();
/**
* Returns the route parameters for the base route the mapper is attached to.
*
* @return array
*/
public function getBaseRouteParameters();
/**
* Returns the base route object the mapper is attached to.
*
* @return \Symfony\Component\Routing\Route
* The base route object the mapper is attached to.
*/
public function getBaseRoute();
/**
* Returns a processed path for the base route the mapper is attached to.
*
* @return string
* Processed path with placeholders replaced.
*/
public function getBasePath();
/**
* Returns route name for the translation overview route.
*
* @return string
* Route name for the mapper.
*/
public function getOverviewRouteName();
/**
* Returns the route parameters for the translation overview route.
*
* @return array
*/
public function getOverviewRouteParameters();
/**
* Returns the route object for a translation overview route.
*
* @return \Symfony\Component\Routing\Route
* The route object for the translation page.
*/
public function getOverviewRoute();
/**
* Returns a processed path for the translation overview route.
*
* @return string
* Processed path with placeholders replaced.
*/
public function getOverviewPath();
/**
* Returns route name for the translation add form route.
*
* @return string
* Route name for the mapper.
*/
public function getAddRouteName();
/**
* Returns the route parameters for the translation add form route.
*
* @return array
*/
public function getAddRouteParameters();
/**
* Returns the route object for a translation add form route.
*
* @return \Symfony\Component\Routing\Route
* The route object for the translation page.
*/
public function getAddRoute();
/**
* Returns route name for the translation edit form route.
*
* @return string
* Route name for the mapper.
*/
public function getEditRouteName();
/**
* Returns the route parameters for the translation edit form route.
*
* @return array
*/
public function getEditRouteParameters();
/**
* Returns the route object for a translation edit form route.
*
* @return \Symfony\Component\Routing\Route
* The route object for the translation page.
*/
public function getEditRoute();
/**
* Returns route name for the translation deletion route.
*
* @return string
* Route name for the mapper.
*/
public function getDeleteRouteName();
/**
* Returns the route parameters for the translation deletion route.
*
* @return array
*/
public function getDeleteRouteParameters();
/**
* Returns the route object for the translation deletion route.
*
* @return \Symfony\Component\Routing\Route
* The route object for the translation page.
*/
public function getDeleteRoute();
/**
* Returns an array of configuration names for the mapper.
*
* @return array
* An array of configuration names for the mapper.
*/
public function getConfigNames();
/**
* Adds the given configuration name to the list of names.
*
* @param string $name
* Configuration name.
*/
public function addConfigName($name);
/**
* Returns the weight of the mapper.
*
* @return int
* The weight of the mapper.
*/
public function getWeight();
/**
* Returns an array with all configuration data.
*
* @return array
* Configuration data keyed by configuration names.
*/
public function getConfigData();
/**
* Returns the original language code of the configuration.
*
* @throws \RuntimeException
* Throws an exception if the language codes in the config files don't
* match.
*/
public function getLangcode();
/**
* Returns language object for the configuration.
*
* 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.
*
* @throws \RuntimeException
* Throws an exception if the language codes in the config files don't
* match.
*/
public function getLanguageWithFallback();
/**
* Returns the name of the type of data the mapper encapsulates.
*
* @return string
* The name of the type of data the mapper encapsulates.
*/
public function getTypeName();
/**
* Provides an array of information to build a list of operation links.
*
* @return array
* An associative array of operation link data for this list, keyed by
* operation name, containing the following key-value pairs:
* - title: The localized title of the operation.
* - href: The path for the operation.
* - options: An array of URL options for the path.
* - weight: The weight of this operation.
*/
public function getOperations();
/**
* Returns the label of the type of data the mapper encapsulates.
*
* @return string
* The label of the type of data the mapper encapsulates.
*/
public function getTypeLabel();
/**
* Checks that all pieces of this configuration mapper have a schema.
*
* @return bool
* TRUE if all of the elements have schema, FALSE otherwise.
*/
public function hasSchema();
/**
* Checks that all pieces of this configuration mapper have translatables.
*
* @return bool
* TRUE if all of the configuration elements have translatables, FALSE
* otherwise.
*/
public function hasTranslatable();
/**
* 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 configuration elements have a translation in the
* given language, FALSE otherwise.
*/
public function hasTranslation(Language $language);
/**
* Populate the config mapper with request data.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* Page request object.
*/
public function populateFromRequest(Request $request);
/**
* Returns the name of the contextual link group to add contextual links to.
*
* @return string|null
* A contextual link group name or null if no link should be added.
*/
public function getContextualLinkGroup();
}
<?php
/**
* @file
* Contains \Drupal\config_translation\ConfigMapperManager.
*/
namespace Drupal\config_translation;
use Drupal\Component\Utility\String;
use Drupal\config_translation\Exception\InvalidMapperDefinitionException;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\Schema\ArrayElement;
use Drupal\Core\Config\TypedConfigManager;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManager;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Plugin\Discovery\InfoHookDecorator;
use Drupal\Core\Plugin\Discovery\YamlDiscovery;
use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
use Drupal\Core\Plugin\Factory\ContainerFactory;
use Drupal\Core\TypedData\TypedDataInterface;
/**
* Manages plugins for configuration translation mappers.
*/
class ConfigMapperManager extends DefaultPluginManager implements ConfigMapperManagerInterface {
/**
* The typed config manager.
*
* @var \Drupal\Core\Config\TypedConfigManager
*/
protected $typedConfigManager;
/**
* {@inheritdoc}
*/
protected $defaults = array(
'title' => '',
'names' => array(),
'weight' => 20,
'class' => '\Drupal\config_translation\ConfigNamesMapper',
'list_controller' => 'Drupal\config_translation\Controller\ConfigTranslationEntityListController',
);
/**
* Constructs a ConfigMapperManager.
*
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* The cache backend.
* @param \Drupal\Core\Language\LanguageManager $language_manager
* The language manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Config\TypedConfigManager $typed_config_manager
* The typed config manager.
*/
public function __construct(CacheBackendInterface $cache_backend, LanguageManager $language_manager, ModuleHandlerInterface $module_handler, TypedConfigManager $typed_config_manager) {
$this->typedConfigManager = $typed_config_manager;
// Look at all themes and modules.
$directories = array();
foreach ($module_handler->getModuleList() as $module => $filename) {
$directories[$module] = dirname($filename);
}
foreach ($this->getThemeList() as $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 InfoHookDecorator($this->discovery, 'config_translation_info');
$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_plugins');
}
/**
* Returns the list of themes on the site.
*
* @param bool $refresh
* Whether to refresh the cached theme list.
*
* @return array
* An associative array of the currently available themes. The keys are the
* themes' machine names and the values are objects. See list_themes() for
* documentation on those objects.
*
* @todo Remove this once https://drupal.org/node/2109287 is fixed in core.
*/
protected function getThemeList($refresh = FALSE) {
return list_themes($refresh);
}
/**
* {@inheritdoc}
*/
public function getMappers() {
$mappers = array();
foreach($this->getDefinitions() as $id => $definition) {
$mappers[$id] = $this->createInstance($id);
}
return $mappers;
}
/**
* {@inheritdoc}
*/
public function processDefinition(&$definition, $plugin_id) {
parent::processDefinition($definition, $plugin_id);
if (!isset($definition['base_route_name'])) {
throw new InvalidMapperDefinitionException($plugin_id, String::format("The plugin definition of the mapper '%plugin_id' does not contain a base_route_name.", array('%plugin_id' => $plugin_id)));
}
if (!is_subclass_of($definition['list_controller'], 'Drupal\config_translation\Controller\ConfigTranslationEntityListControllerInterface')) {
throw new InvalidMapperDefinitionException($plugin_id, String::format("The list_controller '%list_controller' for plugin '%plugin_id' does not implement the expected interface Drupal\config_translation\Controller\ConfigTranslationEntityListControllerInterface.", array('%list_controller' => $definition['list_controller'], '%plugin_id' => $plugin_id)));
}
}
/**
* {@inheritdoc}
*/
public function hasTranslatable($name) {
return $this->findTranslatable($this->typedConfigManager->get($name));
}
/**
* Returns TRUE if at least one translatable element is found.
*
* @param \Drupal\Core\TypedData\TypedDataInterface $element
* Configuration schema element.
*
* @return bool
* A boolean indicating if there is at least one translatable element.
*/
protected function findTranslatable(TypedDataInterface $element) {
// In case this is a sequence or a mapping check whether any child element
// is translatable.
if ($element instanceof ArrayElement) {
foreach ($element as $child_element) {
if ($this->findTranslatable($child_element)) {
return TRUE;
}
}
// If none of the child elements are translatable, return FALSE.
return FALSE;
}
else {
$definition = $element->getDefinition();
return isset($definition['translatable']) && $definition['translatable'];
}
}
}
<?php
/**
* @file
* Contains \Drupal\config_translation\ConfigMapperManagerInterface.
*/
namespace Drupal\config_translation;
use Drupal\Component\Plugin\PluginManagerInterface;
/**
* Provides a common interface for config mapper managers.
*/
interface ConfigMapperManagerInterface extends PluginManagerInterface {
/**
* Returns an array of all mappers.
*
* @return \Drupal\config_translation\ConfigMapperInterface[]
* An array of all mappers.
*/
public function getMappers();
/**
* Returns TRUE if the configuration data has translatable items.
*
* @param string $name
* Configuration key.
*
* @return bool
* A boolean indicating if the configuration data has translatable items.
*/
public function hasTranslatable($name);
}
<?php
/**
* @file
* Contains \Drupal\config_translation\ConfigNamesMapper.
*/
namespace Drupal\config_translation;
use Drupal\Core\Config\ConfigFactory;
use Drupal\Core\Language\Language;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\locale\LocaleConfigManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
/**
* Configuration mapper base implementation.
*/
class ConfigNamesMapper extends PluginBase implements ConfigMapperInterface, ContainerFactoryPluginInterface {
/**
* The configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactory
*/
protected $configFactory;
/**
* The typed configuration manager.
*
* @var \Drupal\locale\LocaleConfigManager
*/
protected $localeConfigManager;
/**
* The mapper plugin discovery service.
*
* @var \Drupal\config_translation\ConfigMapperManagerInterface
*/
protected $configMapperManager;
/**
* The base route object that the mapper is attached to.
*
* @return \Symfony\Component\Routing\Route
*/
protected $baseRoute;
/**
* The language code of the language this mapper, if any.
*
* @var string|null
*/
protected $langcode = NULL;
/**
* Constructs a ConfigNamesMapper.
*
* @param $plugin_id
* The config mapper plugin ID.
* @param array $plugin_definition
* An array of plugin information with the following keys:
* - title: The title of the mapper, used for generating page titles.
* - base_route_name: The route name of the base route this mapper is
* attached to.
* - names: (optional) An array of configuration names.
* - weight: (optional) The weight of this mapper, used in mapper listings.
* Defaults to 20.
* - list_controller: (optional) Class name for list controller used to
* generate lists of this type of configuration.
* @param \Drupal\Core\Config\ConfigFactory $config_factory
* The configuration factory.
* @param \Drupal\locale\LocaleConfigManager $locale_config_manager
* The locale configuration manager.
* @param \Drupal\config_translation\ConfigMapperManagerInterface $config_mapper_manager
* The mapper plugin discovery service.
* @param \Drupal\Core\Routing\RouteProviderInterface
* The route provider.
* @param \Drupal\Core\StringTranslation\TranslationInterface $translation_manager
* The string translation manager.
*
* @throws \Symfony\Component\Routing\Exception\RouteNotFoundException
* Throws an exception if the route specified by the 'base_route_name' in
* the plugin definition could not be found by the route provider.
*/
public function __construct($plugin_id, array $plugin_definition, ConfigFactory $config_factory, LocaleConfigManager $locale_config_manager, ConfigMapperManagerInterface $config_mapper_manager, RouteProviderInterface $route_provider, TranslationInterface $translation_manager) {
$this->pluginId = $plugin_id;
$this->pluginDefinition = $plugin_definition;
$this->configFactory = $config_factory;
$this->localeConfigManager = $locale_config_manager;
$this->configMapperManager = $config_mapper_manager;
$this->setTranslationManager($translation_manager);
$this->baseRoute = $route_provider->getRouteByName($this->getBaseRouteName());
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) {
// Note that we ignore the plugin $configuration because mappers have
// nothing to configure in themselves.
return new static (
$plugin_id,
$plugin_definition,
$container->get('config.factory'),
$container->get('locale.config.typed'),
$container->get('plugin.manager.config_translation.mapper'),
$container->get('router.route_provider'),
$container->get('string_translation')
);
}
/**
* {@inheritdoc}
*/
public function getTitle() {
// A title from a *.config_translation.yml. Should be translated for
// display in the current page language.
return $this->t($this->pluginDefinition['title']);
}
/**
* {@inheritdoc}
*/
public function getBaseRouteName() {
return $this->pluginDefinition['base_route_name'];
}
/**
* {@inheritdoc}
*/
public function getBaseRouteParameters() {
return array();
}
/**
* {@inheritdoc}
*/
public function getBaseRoute() {
return $this->baseRoute;
}
/**
* {@inheritdoc}
*/
public function getBasePath() {
return $this->getPathFromRoute($this->getBaseRoute(), $this->getBaseRouteParameters());
}
/**
* {@inheritdoc}
*/
public function getOverviewRouteName() {
return 'config_translation.item.overview.' . $this->getBaseRouteName();
}
/**
* {@inheritdoc}
*/
public function getOverviewRouteParameters() {
return $this->getBaseRouteParameters();
}
/**
* {@inheritdoc}
*/
public function getOverviewRoute() {
return new Route(
$this->getBaseRoute()->getPath() . '/translate',
array(
'_controller' => '\Drupal\config_translation\Controller\ConfigTranslationController::itemPage',
'plugin_id' => $this->getPluginId(),
),
array('_config_translation_overview_access' => 'TRUE')
);
}
/**
* {@inheritdoc}
*/
public function getOverviewPath() {
return $this->getPathFromRoute($this->getOverviewRoute(), $this->getOverviewRouteParameters());
}
/**
* {@inheritdoc}
*/
public function getAddRouteName() {
return 'config_translation.item.add.' . $this->getBaseRouteName();
}
/**
* {@inheritdoc}
*/
public function getAddRouteParameters() {
// If sub-classes provide route parameters in getBaseRouteParameters(), they
// probably also want to provide those for the add, edit, and delete forms.
$parameters = $this->getBaseRouteParameters();
$parameters['langcode'] = $this->langcode;
return $parameters;
}
/**
* {@inheritdoc}
*/
public function getAddRoute() {
return new Route(
$this->getBaseRoute()->getPath() . '/translate/{langcode}/add',
array(
'_form' => '\Drupal\config_translation\Form\ConfigTranslationAddForm',
'plugin_id' => $this->getPluginId(),
),
array('_config_translation_form_access' => 'TRUE')
);
}
/**
* {@inheritdoc}
*/
public function getEditRouteName() {
return 'config_translation.item.edit.' . $this->getBaseRouteName();
}
/**
* {@inheritdoc}
*/
public function getEditRouteParameters() {
return $this->getAddRouteParameters();
}
/**
* {@inheritdoc}
*/
public function getEditRoute() {
return new Route(
$this->getBaseRoute()->getPath() . '/translate/{langcode}/edit',
array(
'_form' => '\Drupal\config_translation\Form\ConfigTranslationEditForm',
'plugin_id' => $this->getPluginId(),
),
array('_config_translation_form_access' => 'TRUE')
);
}
/**
* {@inheritdoc}
*/
public function getDeleteRouteName() {
return 'config_translation.item.delete.' . $this->getBaseRouteName();
}
/**
* {@inheritdoc}
*/
public function getDeleteRouteParameters() {
return $this->getAddRouteParameters();
}
/**
* {@inheritdoc}
*/
public function getDeleteRoute() {
return new Route(
$this->getBaseRoute()->getPath() . '/translate/{langcode}/delete',
array(
'_form' => '\Drupal\config_translation\Form\ConfigTranslationDeleteForm',
'plugin_id' => $this->getPluginId(),
),
array('_config_translation_form_access' => 'TRUE')
);
}
/**
* Gets the path for a certain route, given a set of route parameters.
*
* @param \Symfony\Component\Routing\Route $route
* The route object.
* @param array $parameters
* An array of route parameters.
*
* @return string
* Processed path with placeholders replaced.
*/
public function getPathFromRoute(Route $route, array $parameters) {
$path = $route->getPath();
foreach ($parameters as $key => $value) {
$path = str_replace('{' . $key . '}', $value, $path);
}
return $path;
}
/**
* {@inheritdoc}
*/
public function getConfigNames() {
return $this->pluginDefinition['names'];
}
/**
* {@inheritdoc}
*/
public function addConfigName($name) {
$this->pluginDefinition['names'][] = $name;
}
/**
* {@inheritdoc}
*/
public function getWeight() {
return $this->pluginDefinition['weight'];
}
/**
* {@inheritdoc}
*/
public function populateFromRequest(Request $request) {
if ($request->attributes->has('langcode')) {
$this->langcode = $request->attributes->get('langcode');
}
else {
$this->langcode = NULL;
}
}
/**
* {@inheritdoc}
*/
public function getTypeLabel() {
return $this->getTitle();
}
/**
* {@inheritdoc}
*/
public function getLangcode() {
$config_factory = $this->configFactory;
$langcodes = array_map(function($name) use ($config_factory) {
// Default to English if no language code was provided in the file.
// Although it is a best practice to include a language code, if the
// developer did not think about a multilingual use-case, we fall back
// on assuming the file is English.
return $config_factory->get($name)->get('langcode') ?: 'en';
}, $this->getConfigNames());
if (count(array_unique($langcodes)) > 1) {
throw new \RuntimeException('A config mapper can only contain configuration for a single language.');
}
return reset($langcodes);
}
/**
* {@inheritdoc}
*/
public function getLanguageWithFallback() {
$langcode = $this->getLangcode();
$language = language_load($langcode);
// If the language of the file is English but English is not a configured
// language on the site, create a mock language object to represent this
// language run-time. In this case, the title of the language is
// 'Built-in English' because we assume such configuration is shipped with
// core and the modules and not custom created. (In the later case an
// English language configured on the site is assumed.)
if (empty($language) && $langcode == 'en') {
$language = new Language(array('id' => 'en', 'name' => $this->t('Built-in English')));
}
return $language;
}
/**
* {@inheritdoc}
*/
public function getConfigData() {
$config_data = array();
foreach ($this->getConfigNames() as $name) {
$config_data[$name] = $this->configFactory->get($name)->get();
}
return $config_data;
}
/**
* {@inheritdoc}
*/
public function hasSchema() {
foreach ($this->getConfigNames() as $name) {
if (!$this->localeConfigManager->hasConfigSchema($name)) {
return FALSE;
}
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function hasTranslatable() {
foreach ($this->getConfigNames() as $name) {
if (!$this->configMapperManager->hasTranslatable($name)) {
return FALSE;
}
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function hasTranslation(Language $language) {
foreach ($this->getConfigNames() as $name) {
if ($this->localeConfigManager->hasTranslation($name, $language)) {
return TRUE;
}
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getTypeName() {
return $this->t('Settings');
}
/**
* {@inheritdoc}
*/
public function getOperations() {
return array(
'translate' => array(
'title' => $this->t('Translate'),
'href' => $this->getOverviewPath(),
),
);
}
/**
* {@inheritdoc}
*/
public function getContextualLinkGroup() {
return NULL;
}
}