Commit cc7cae5f authored by webchick's avatar webchick

Issue #1952394 by vijaycs85, tstoeckler, webflo, Gábor Hojtsy, Schnitzel,...

Issue #1952394 by vijaycs85, tstoeckler, webflo, Gábor Hojtsy, Schnitzel, falcon03, YesCT, kfritsche, Ryan Weal, dagmita, likin, toddtomlinson, nonsie, Kristen Pol, dawehner, tim.plunkett, penyaskito, EclipseGC, larowlan, robertdbailey, helenkim, David Hernández, EllaTheHarpy, lazysoundsystem, juanolalla, R.Hendel, Kartagis: Add configuration translation user interface module.
parent 9c306c1c
......@@ -237,6 +237,11 @@ Comment module
Configuration module
- ?
Configuration translation module
- Gábor Hojtsy 'Gábor Hojtsy' http://drupal.org/user/4166
- Tobias Stöckler 'tstoeckler' https://drupal.org/user/107158
- Vijayachandran Mani 'vijaycs85' https://drupal.org/user/93488
Contact module
- ?
......
<?php
/**
* @file
* Hooks provided by the Configuration Translation module.
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Introduce dynamic translation tabs for translation of 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.
*
* Note that while themes can provide THEME.config_translation.yml files this
* hook is not invoked for themes.
*
* @param array $info
* An associative array of configuration mapper information. 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.
*
* @see hook_config_translation_info_alter()
* @see \Drupal\config_translation\ConfigMapperManagerInterface
* @see \Drupal\config_translation\Routing\RouteSubscriber::routes()
*/
function hook_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' => t('!label field'),
'class' => '\Drupal\config_translation\ConfigFieldInstanceMapper',
'base_entity_type' => $entity_type,
'list_controller' => '\Drupal\config_translation\Controller\ConfigTranslationFieldInstanceListController',
'weight' => 10,
);
}
}
}
}
/**
* Alter existing translation tabs for translation of configuration.
*
* This hook is useful to extend existing configuration 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.
*
* @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.
*
* @see hook_translation_info()
* @see \Drupal\config_translation\ConfigMapperManagerInterface
*/
function hook_config_translation_info_alter(&$info) {
// Add additional site settings to the site information screen, so it shows
// up on the translation screen. (Form alter in the elements whose values are
// stored in this config file using regular form altering on the original
// configuration form.)
$info['system.site_information_settings']['names'][] = 'example.site.setting';
}
/**
* Alter config typed data definitions.
*
* Used to automatically generate translation forms, you can alter the typed
* data types representing each configuration schema type to change default
* labels or form element renderers.
*
* @param $definitions
* Associative array of configuration type definitions keyed by schema type
* names. The elements are themselves array with information about the type.
*/
function hook_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';
}
/**
* @} End of "addtogroup hooks".
*/
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;
}