summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorwebchick2013-11-18 02:41:04 (GMT)
committerwebchick2013-11-18 02:41:21 (GMT)
commitcc7cae5fe14ac9abe62e84467cd09534c269c913 (patch)
treecf52ddbfc46957ab16b755987e14969952800331
parent9c306c1cb68c49634e558dd528080c5887371bd9 (diff)
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.
-rw-r--r--core/MAINTAINERS.txt5
-rw-r--r--core/modules/config_translation/config_translation.api.php115
-rw-r--r--core/modules/config_translation/config_translation.contextual_links.yml4
-rw-r--r--core/modules/config_translation/config_translation.info.yml8
-rw-r--r--core/modules/config_translation/config_translation.local_tasks.yml4
-rw-r--r--core/modules/config_translation/config_translation.module186
-rw-r--r--core/modules/config_translation/config_translation.routing.yml14
-rw-r--r--core/modules/config_translation/config_translation.services.yml26
-rw-r--r--core/modules/config_translation/css/config_translation.admin.css38
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigTranslationFormAccess.php50
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigTranslationOverviewAccess.php74
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/ConfigEntityMapper.php237
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/ConfigFieldInstanceMapper.php44
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperInterface.php288
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperManager.php162
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperManagerInterface.php36
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/ConfigNamesMapper.php451
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationBlockListController.php92
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationController.php252
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationEntityListController.php130
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationEntityListControllerInterface.php30
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationFieldInstanceListController.php192
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationListController.php85
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationMapperList.php137
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/Exception/InvalidMapperDefinitionException.php47
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationAddForm.php44
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationDeleteForm.php161
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationEditForm.php43
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationFormBase.php408
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/FormElement/DateFormat.php78
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/FormElement/Element.php44
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/FormElement/ElementInterface.php32
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/FormElement/Textarea.php35
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/FormElement/Textfield.php29
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/Plugin/Derivative/ConfigTranslationContextualLinks.php73
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/Plugin/Derivative/ConfigTranslationLocalTasks.php116
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/Plugin/Menu/ContextualLink/ConfigTranslationContextualLink.php55
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/Plugin/Menu/LocalTask/ConfigTranslationLocalTask.php55
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/Routing/RouteSubscriber.php49
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationFormTest.php79
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationListUiTest.php487
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationOverviewTest.php131
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUiTest.php741
-rw-r--r--core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationViewListUiTest.php66
-rw-r--r--core/modules/config_translation/templates/config_translation_manage_form_element.html.twig24
-rw-r--r--core/modules/config_translation/tests/Drupal/config_translation/Tests/ConfigEntityMapperTest.php193
-rw-r--r--core/modules/config_translation/tests/Drupal/config_translation/Tests/ConfigMapperManagerTest.php213
-rw-r--r--core/modules/config_translation/tests/Drupal/config_translation/Tests/ConfigNamesMapperTest.php656
-rw-r--r--core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.info.yml9
-rw-r--r--core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module80
-rw-r--r--core/modules/config_translation/tests/themes/config_translation_test_theme/config_translation_test_theme.config_translation.yml5
-rw-r--r--core/modules/config_translation/tests/themes/config_translation_test_theme/config_translation_test_theme.info.yml6
-rw-r--r--core/modules/system/system.config_translation.yml17
-rw-r--r--core/modules/user/user.config_translation.yml6
54 files changed, 6642 insertions, 0 deletions
diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt
index 59eb6c5..4297d57 100644
--- a/core/MAINTAINERS.txt
+++ b/core/MAINTAINERS.txt
@@ -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
- ?
diff --git a/core/modules/config_translation/config_translation.api.php b/core/modules/config_translation/config_translation.api.php
new file mode 100644
index 0000000..e8151b3
--- /dev/null
+++ b/core/modules/config_translation/config_translation.api.php
@@ -0,0 +1,115 @@
+<?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".
+ */
diff --git a/core/modules/config_translation/config_translation.contextual_links.yml b/core/modules/config_translation/config_translation.contextual_links.yml
new file mode 100644
index 0000000..ecdc1cd
--- /dev/null
+++ b/core/modules/config_translation/config_translation.contextual_links.yml
@@ -0,0 +1,4 @@
+config_translation.contextual_links:
+ title: 'Translate @type_name'
+ derivative: 'Drupal\config_translation\Plugin\Derivative\ConfigTranslationContextualLinks'
+ weight: 100
diff --git a/core/modules/config_translation/config_translation.info.yml b/core/modules/config_translation/config_translation.info.yml
new file mode 100644
index 0000000..aebc7f4
--- /dev/null
+++ b/core/modules/config_translation/config_translation.info.yml
@@ -0,0 +1,8 @@
+name: 'Configuration Translation'
+type: module
+description: 'Provides a translation interface for configuration.'
+package: Multilingual
+version: VERSION
+core: 8.x
+dependencies:
+ - locale
diff --git a/core/modules/config_translation/config_translation.local_tasks.yml b/core/modules/config_translation/config_translation.local_tasks.yml
new file mode 100644
index 0000000..304d557
--- /dev/null
+++ b/core/modules/config_translation/config_translation.local_tasks.yml
@@ -0,0 +1,4 @@
+config_translation.local_tasks:
+ title: 'Translate @type_name'
+ derivative: 'Drupal\config_translation\Plugin\Derivative\ConfigTranslationLocalTasks'
+ weight: 100
diff --git a/core/modules/config_translation/config_translation.module b/core/modules/config_translation/config_translation.module
new file mode 100644
index 0000000..0fc1170
--- /dev/null
+++ b/core/modules/config_translation/config_translation.module
@@ -0,0 +1,186 @@
+<?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);
+}
diff --git a/core/modules/config_translation/config_translation.routing.yml b/core/modules/config_translation/config_translation.routing.yml
new file mode 100644
index 0000000..dd6b71f
--- /dev/null
+++ b/core/modules/config_translation/config_translation.routing.yml
@@ -0,0 +1,14 @@
+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'
diff --git a/core/modules/config_translation/config_translation.services.yml b/core/modules/config_translation/config_translation.services.yml
new file mode 100644
index 0000000..f30e569
--- /dev/null
+++ b/core/modules/config_translation/config_translation.services.yml
@@ -0,0 +1,26 @@
+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'
diff --git a/core/modules/config_translation/css/config_translation.admin.css b/core/modules/config_translation/css/config_translation.admin.css
new file mode 100644
index 0000000..66d9ef3
--- /dev/null
+++ b/core/modules/config_translation/css/config_translation.admin.css
@@ -0,0 +1,38 @@
+/**
+ * @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;
+ }
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigTranslationFormAccess.php b/core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigTranslationFormAccess.php
new file mode 100644
index 0000000..5e98d41
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigTranslationFormAccess.php
@@ -0,0 +1,50 @@
+<?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;
+ }
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigTranslationOverviewAccess.php b/core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigTranslationOverviewAccess.php
new file mode 100644
index 0000000..88a94ab
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Access/ConfigTranslationOverviewAccess.php
@@ -0,0 +1,74 @@
+<?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;
+ }
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/ConfigEntityMapper.php b/core/modules/config_translation/lib/Drupal/config_translation/ConfigEntityMapper.php
new file mode 100644
index 0000000..44e0d7f
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/ConfigEntityMapper.php
@@ -0,0 +1,237 @@
+<?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;
+ }
+ }
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/ConfigFieldInstanceMapper.php b/core/modules/config_translation/lib/Drupal/config_translation/ConfigFieldInstanceMapper.php
new file mode 100644
index 0000000..ce4f1be
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/ConfigFieldInstanceMapper.php
@@ -0,0 +1,44 @@
+<?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();
+ // @todo All core content entity path placeholders can be fully filled in
+ // with an additional {bundle} value in their paths, but a more
+ // predictable solution would be ideal. See
+ // https://drupal.org/node/2134871
+ // @todo Field instances have no method to return the bundle the instance is
+ // attached to. See https://drupal.org/node/2134861
+ $parameters['bundle'] = $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']));
+ }
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperInterface.php b/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperInterface.php
new file mode 100644
index 0000000..b572708
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperInterface.php
@@ -0,0 +1,288 @@
+<?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();
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperManager.php b/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperManager.php
new file mode 100644
index 0000000..edb431c
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperManager.php
@@ -0,0 +1,162 @@
+<?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'];
+ }
+ }
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperManagerInterface.php b/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperManagerInterface.php
new file mode 100644
index 0000000..693f25b
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/ConfigMapperManagerInterface.php
@@ -0,0 +1,36 @@
+<?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);
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/ConfigNamesMapper.php b/core/modules/config_translation/lib/Drupal/config_translation/ConfigNamesMapper.php
new file mode 100644
index 0000000..2df7387
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/ConfigNamesMapper.php
@@ -0,0 +1,451 @@
+<?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;
+ }
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationBlockListController.php b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationBlockListController.php
new file mode 100644
index 0000000..bd0f6f5
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationBlockListController.php
@@ -0,0 +1,92 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Controller\ConfigTranslationBlockListController.
+ */
+
+namespace Drupal\config_translation\Controller;
+
+use Drupal\Component\Utility\String;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+
+/**
+ * Defines the config translation controller for blocks.
+ */
+class ConfigTranslationBlockListController extends ConfigTranslationEntityListController {
+
+ /**
+ * An array of theme info keyed by theme name.
+ *
+ * @var array
+ */
+ protected $themes = array();
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct($entity_type, array $entity_info, EntityStorageControllerInterface $storage, ModuleHandlerInterface $module_handler) {
+ parent::__construct($entity_type, $entity_info, $storage, $module_handler);
+ $this->themes = list_themes();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFilterLabels() {
+ $info = parent::getFilterLabels();
+
+ $info['placeholder'] = $this->t('Enter block, theme or category');
+ $info['description'] = $this->t('Enter a part of the block, theme or category to filter by.');
+
+ return $info;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildRow(EntityInterface $entity) {
+ $theme = $entity->get('theme');
+ $plugin_definition = $entity->getPlugin()->getPluginDefinition();
+
+ $row['label'] = array(
+ 'data' => $this->getLabel($entity),
+ 'class' => 'table-filter-text-source',
+ );
+
+ $row['theme'] = array(
+ 'data' => String::checkPlain($this->themes[$theme]->info['name']),
+ 'class' => 'table-filter-text-source',
+ );
+
+ $row['category'] = array(
+ 'data' => String::checkPlain($plugin_definition['category']),
+ 'class' => 'table-filter-text-source',
+ );
+
+ $row['operations']['data'] = $this->buildOperations($entity);
+
+ return $row;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildHeader() {
+ $header['label'] = $this->t('Block');
+ $header['theme'] = $this->t('Theme');
+ $header['category'] = $this->t('Category');
+ $header['operations'] = $this->t('Operations');
+ return $header;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function sortRows($a, $b) {
+ return $this->sortRowsMultiple($a, $b, array('theme', 'category', 'label'));
+ }
+
+}
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
new file mode 100644
index 0000000..9867def
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationController.php
@@ -0,0 +1,252 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Controller\ConfigTranslationController.
+ */
+
+namespace Drupal\config_translation\Controller;
+
+use Drupal\config_translation\ConfigMapperManagerInterface;
+use Drupal\Core\Access\AccessManager;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Language\Language;
+use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Symfony\Component\Routing\Exception\ResourceNotFoundException;
+use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Provides page callbacks for the configuration translation interface.
+ */
+class ConfigTranslationController extends ControllerBase implements ContainerInjectionInterface {
+
+ /**
+ * Configuration mapper manager.
+ *
+ * @var \Drupal\config_translation\ConfigMapperManagerInterface
+ */
+ protected $configMapperManager;
+
+ /**
+ * The menu link access service.
+ *
+ * @var \Drupal\Core\Access\AccessManager
+ */
+ protected $accessManager;
+
+ /**
+ * The dynamic router service.
+ *
+ * @var \Symfony\Component\Routing\Matcher\RequestMatcherInterface
+ */
+ protected $router;
+
+ /**
+ * The path processor service.
+ *
+ * @var \Drupal\Core\PathProcessor\InboundPathProcessorInterface
+ */
+ protected $pathProcessor;
+
+ /**
+ * The current user.
+ *
+ * @var \Drupal\Core\Session\AccountInterface
+ */
+ protected $account;
+
+ /**
+ * Constructs a ConfigTranslationController.
+ *
+ * @param \Drupal\config_translation\ConfigMapperManagerInterface $config_mapper_manager
+ * The configuration mapper manager.
+ * @param \Drupal\Core\Access\AccessManager $access_manager
+ * The menu link access service.
+ * @param \Symfony\Component\Routing\Matcher\RequestMatcherInterface $router
+ * The dynamic router service.
+ * @param \Drupal\Core\PathProcessor\InboundPathProcessorInterface $path_processor
+ * The inbound path processor.
+ * @param \Drupal\Core\Session\AccountInterface $account
+ * The current user.
+ */
+ public function __construct(ConfigMapperManagerInterface $config_mapper_manager, AccessManager $access_manager, RequestMatcherInterface $router, InboundPathProcessorInterface $path_processor, AccountInterface $account) {
+ $this->configMapperManager = $config_mapper_manager;
+ $this->accessManager = $access_manager;
+ $this->router = $router;
+ $this->pathProcessor = $path_processor;
+ $this->account = $account;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('plugin.manager.config_translation.mapper'),
+ $container->get('access_manager'),
+ $container->get('router'),
+ $container->get('path_processor_manager'),
+ $container->get('current_user')
+ );
+ }
+
+ /**
+ * Language translations overview page for a configuration name.
+ *
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ * Page request object.
+ * @param string $plugin_id
+ * The plugin ID of the mapper.
+ *
+ * @return array
+ * Page render array.
+ */
+ public function itemPage(Request $request, $plugin_id) {
+ /** @var \Drupal\config_translation\ConfigMapperInterface $mapper */
+ $mapper = $this->configMapperManager->createInstance($plugin_id);
+ $mapper->populateFromRequest($request);
+
+ $page = array();
+ $page['#title'] = $this->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 = $mapper->getLangcode();
+ if (!isset($languages[$original_langcode])) {
+ $language_name = language_name($original_langcode);
+ if ($original_langcode == 'en') {
+ $language_name = $this->t('Built-in English');
+ }
+ // Create a dummy language object for this listing only.
+ $languages[$original_langcode] = new Language(array('id' => $original_langcode, 'name' => $language_name));
+ }
+
+ // We create a fake request object to pass into
+ // ConfigMapperInterface::populateFromRequest() for the different languages.
+ // Creating a separate request for each language and route is neither easily
+ // possible nor performant.
+ $fake_request = $request->duplicate();
+
+ $page['languages'] = array(
+ '#type' => 'table',
+ '#header' => array($this->t('Language'), $this->t('Operations')),
+ );
+ foreach ($languages as $language) {
+ $langcode = $language->id;
+
+ // This is needed because e.g.
+ // ConfigMapperInterface::getAddRouteParameters()
+ // needs to return the correct language code for each table row.
+ $fake_request->attributes->set('langcode', $langcode);
+ $mapper->populateFromRequest($fake_request);
+
+ // Prepare the language name and the operations depending on whether this
+ // is the original language or not.
+ if ($langcode == $original_langcode) {
+ $language_name = '<strong>' . $this->t('@language (original)', array('@language' => $language->name)) . '</strong>';
+
+ // Check access for the path/route for editing, so we can decide to
+ // include a link to edit or not.
+ $route_request = $this->getRequestForPath($request, $mapper->getBasePath());
+ $edit_access = FALSE;
+ if (!empty($route_request)) {
+ $route_name = $route_request->attributes->get(RouteObjectInterface::ROUTE_NAME);
+ // Note that the parameters don't really matter here since we're
+ // passing in the request which already has the upcast attributes.
+ $parameters = array();
+ $edit_access = $this->accessManager->checkNamedRoute($route_name, $parameters, $this->account, $route_request);
+ }
+
+ // Build list of operations.
+ $operations = array();
+ if ($edit_access) {
+ $operations['edit'] = array(
+ 'title' => $this->t('Edit'),
+ 'route_name' => $mapper->getBaseRouteName(),
+ 'route_parameters' => $mapper->getBaseRouteParameters(),
+ 'query' => array('destination' => $mapper->getOverviewPath()),
+ );
+ }
+ }
+ else {
+ $language_name = $language->name;
+
+ $operations = array();
+ // If no translation exists for this language, link to add one.
+ if (!$mapper->hasTranslation($language)) {
+ $operations['add'] = array(
+ 'title' => $this->t('Add'),
+ 'route_name' => $mapper->getAddRouteName(),
+ 'route_parameters' => $mapper->getAddRouteParameters(),
+ );
+ }
+ else {
+ // Otherwise, link to edit the existing translation.
+ $operations['edit'] = array(
+ 'title' => $this->t('Edit'),
+ 'route_name' => $mapper->getEditRouteName(),
+ 'route_parameters' => $mapper->getEditRouteParameters(),
+ );
+
+ $operations['delete'] = array(
+ 'title' => $this->t('Delete'),
+ 'route_name' => $mapper->getDeleteRouteName(),
+ 'route_parameters' => $mapper->getDeleteRouteParameters(),
+ );
+ }
+ }
+
+ $page['languages'][$langcode]['language'] = array(
+ '#markup' => $language_name,
+ );
+
+ $page['languages'][$langcode]['operations'] = array(
+ '#type' => 'operations',
+ '#links' => $operations,
+ );
+ }
+ return $page;
+ }
+
+ /**
+ * Matches a path in the router.
+ *
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ * Page request object.
+ * @param string $path
+ * Path to look up.
+ *
+ * @return \Symfony\Component\HttpFoundation\Request|null
+ * A populated request object or NULL if the patch couldn't be matched.
+ */
+ protected function getRequestForPath(Request $request, $path) {
+ // @todo Use RequestHelper::duplicate once https://drupal.org/node/2090293
+ // is fixed.
+ $route_request = Request::create($request->getBaseUrl() . '/' . $path);
+ // Find the system path by resolving aliases, language prefix, etc.
+ $processed = $this->pathProcessor->processInbound($path, $route_request);
+ $route_request->attributes->set('_system_path', $processed);
+ // Attempt to match this path to provide a fully built request.
+ try {
+ $route_request->attributes->add($this->router->matchRequest($route_request));
+ return $route_request;
+ }
+ catch (NotFoundHttpException $e) {
+ return NULL;
+ }
+ catch (ResourceNotFoundException $e) {
+ return NULL;
+ }
+ }
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationEntityListController.php b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationEntityListController.php
new file mode 100644
index 0000000..09a9de6
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationEntityListController.php
@@ -0,0 +1,130 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Controller\ConfigTranslationEntityListController.
+ */
+
+namespace Drupal\config_translation\Controller;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityListController;
+
+/**
+ * Defines the configuration translation controller for entities.
+ */
+class ConfigTranslationEntityListController extends EntityListController implements ConfigTranslationEntityListControllerInterface {
+
+ /**
+ * Provides user facing strings for the filter element.
+ *
+ * @return array
+ */
+ protected function getFilterLabels() {
+ return array(
+ 'placeholder' => $this->t('Enter label'),
+ 'description' => $this->t('Enter a part of the label or description to filter by.'),
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render() {
+ $table = parent::render();
+ $filter = $this->getFilterLabels();
+
+ usort($table['#rows'], array($this, 'sortRows'));
+
+ $build['filters'] = array(
+ '#type' => 'container',
+ '#attributes' => array(
+ 'class' => array('table-filter', 'js-show'),
+ ),
+ );
+
+ $build['filters']['text'] = array(
+ '#type' => 'search',
+ '#title' => $this->t('Search'),
+ '#size' => 30,
+ '#placeholder' => $filter['placeholder'],
+ '#attributes' => array(
+ 'class' => array('table-filter-text'),
+ 'data-table' => '.config-translation-entity-list',
+ 'autocomplete' => 'off',
+ 'title' => $filter['description'],
+ ),
+ );
+
+ $build['table'] = $table;
+ $build['table']['#attributes']['class'][] = 'config-translation-entity-list';
+ $build['#attached']['library'][] = array('system', 'drupal.system.modules');
+
+ return $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildRow(EntityInterface $entity) {
+ $row['label']['data'] = $this->getLabel($entity);
+ $row['label']['class'] = 'table-filter-text-source';
+ return $row + parent::buildRow($entity);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildHeader() {
+ $header['label'] = $this->t('Label');
+ return $header + parent::buildHeader();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildOperations(EntityInterface $entity) {
+ $operations = parent::buildOperations($entity);
+ foreach (array_keys($operations['#links']) as $operation) {
+ // This is a translation UI for translators. Show the translation
+ // operation only.
+ if (!($operation == 'translate')) {
+ unset($operations['#links'][$operation]);
+ }
+ }
+ return $operations;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function sortRows($a, $b) {
+ return $this->sortRowsMultiple($a, $b, array('label'));
+ }
+
+ /**
+ * Sorts an array by multiple criteria.
+ *
+ * @param array $a
+ * First item for comparison.
+ * @param array $b
+ * Second item for comparison.
+ * @param array $keys
+ * The array keys to sort on.
+ *
+ * @return int
+ * The comparison result for uasort().
+ */
+ protected function sortRowsMultiple($a, $b, $keys) {
+ $key = array_shift($keys);
+ $a_value = (is_array($a) && isset($a[$key]['data'])) ? $a[$key]['data'] : '';
+ $b_value = (is_array($b) && isset($b[$key]['data'])) ? $b[$key]['data'] : '';
+
+ if ($a_value == $b_value && !empty($keys)) {
+ return $this->sortRowsMultiple($a, $b, $keys);
+ }
+
+ return strnatcasecmp($a_value, $b_value);
+ }
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationEntityListControllerInterface.php b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationEntityListControllerInterface.php
new file mode 100644
index 0000000..d342808
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationEntityListControllerInterface.php
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Controller\ConfigTranslationEntityListControllerInterface.
+ */
+
+namespace Drupal\config_translation\Controller;
+
+use Drupal\Core\Entity\EntityListControllerInterface;
+
+/**
+ * Defines an interface for configuration translation entity list controllers.
+ */
+interface ConfigTranslationEntityListControllerInterface extends EntityListControllerInterface {
+
+ /**
+ * Sorts an array by value.
+ *
+ * @param array $a
+ * First item for comparison.
+ * @param array $b
+ * Second item for comparison.
+ *
+ * @return int
+ * The comparison result for uasort().
+ */
+ public function sortRows($a, $b);
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationFieldInstanceListController.php b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationFieldInstanceListController.php
new file mode 100644
index 0000000..d305988
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationFieldInstanceListController.php
@@ -0,0 +1,192 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Controller\ConfigTranslationFieldInstanceListController.
+ */
+
+namespace Drupal\config_translation\Controller;
+
+use Drupal\Component\Utility\String;
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityManager;
+use Drupal\Core\Entity\EntityStorageControllerInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\field\Field;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines the config translation controller for field instance entities.
+ */
+class ConfigTranslationFieldInstanceListController extends ConfigTranslationEntityListController {
+
+ /**
+ * The name of the entity type the field instances are attached to.
+ *
+ * @var string
+ */
+ protected $baseEntityType = '';
+
+ /**
+ * An array containing the base entity type's definition.
+ *
+ * @var string
+ */
+ protected $baseEntityInfo = array();
+
+ /**
+ * The bundle info for the base entity type.
+ *
+ * @var string
+ */
+ protected $baseEntityBundles = array();
+
+ /**
+ * The entity manager.
+ *
+ * @var \Drupal\Core\Entity\EntityManager
+ */
+ protected $entityManager;
+
+ /**
+ * Instantiates a new instance of this entity controller.
+ *
+ * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
+ * The service container this object should use.
+ * @param string $entity_type
+ * The entity type which the controller handles.
+ * @param array $entity_info
+ * An array of entity info for the entity type.
+ * @param array $definition
+ * The plugin definition of the config translation mapper.
+ *
+ * @return static
+ * A new instance of the entity controller.
+ */
+ public static function createInstance(ContainerInterface $container, $entity_type, array $entity_info, array $definition = array()) {
+ return new static(
+ $entity_type,
+ $entity_info,
+ $container->get('entity.manager')->getStorageController($entity_type),
+ $container->get('module_handler'),
+ $container->get('entity.manager'),
+ $definition
+ );
+ }
+
+ /**
+ * Constructs a new EntityListController object.
+ *
+ * @param string $entity_type
+ * The type of entity to be listed.
+ * @param array $entity_info
+ * An array of entity info for the entity type.
+ * @param \Drupal\Core\Entity\EntityStorageControllerInterface $storage
+ * The entity storage controller class.
+ * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+ * The module handler to invoke hooks on.
+ * @param \Drupal\Core\Entity\EntityManager $entity_manager
+ * The entity manager.
+ * @param $definition
+ * The plugin definition of the config translation mapper.
+ */
+ public function __construct($entity_type, array $entity_info, EntityStorageControllerInterface $storage, ModuleHandlerInterface $module_handler, EntityManager $entity_manager, array $definition) {
+ parent::__construct($entity_type, $entity_info, $storage, $module_handler);
+ $this->entityManager = $entity_manager;
+ $this->baseEntityType = $definition['base_entity_type'];
+ $this->baseEntityInfo = $this->entityManager->getDefinition($this->baseEntityType);
+ $this->baseEntityBundles = $this->entityManager->getBundleInfo($this->baseEntityType);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function load() {
+ $entities = array();
+ // It is not possible to use the standard load method, because this needs
+ // all field instance entities only for the given baseEntityType.
+ foreach (Field::fieldInfo()->getInstances($this->baseEntityType) as $fields) {
+ $entities = array_merge($entities, array_values($fields));
+ }
+ return $entities;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFilterLabels() {
+ $info = parent::getFilterLabels();
+ $bundle = isset($this->baseEntityInfo['bundle_label']) ? $this->baseEntityInfo['bundle_label'] : $this->t('Bundle');
+ $bundle = Unicode::strtolower($bundle);
+
+ $info['placeholder'] = $this->t('Enter field or @bundle', array('@bundle' => $bundle));
+ $info['description'] = $this->t('Enter a part of the field or @bundle to filter by.', array('@bundle' => $bundle));
+
+ return $info;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildRow(EntityInterface $entity) {
+ $row['label'] = array(
+ 'data' => $this->getLabel($entity),
+ 'class' => 'table-filter-text-source',
+ );
+
+ if ($this->displayBundle()) {
+ $bundle = $entity->get('bundle');
+ $row['bundle'] = array(
+ 'data' => String::checkPlain($this->baseEntityBundles[$bundle]['label']),
+ 'class' => 'table-filter-text-source',
+ );
+ }
+
+ return $row + parent::buildRow($entity);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildHeader() {
+ $header['label'] = $this->t('Field');
+ if ($this->displayBundle()) {
+ $header['bundle'] = isset($this->baseEntityInfo['bundle_label']) ? $this->baseEntityInfo['bundle_label'] : $this->t('Bundle');
+ }
+ return $header + parent::buildHeader();
+ }
+
+ /**
+ * Controls the visibility of the bundle column on field instance list pages.
+ *
+ * @return bool
+ * Whenever the bundle is displayed or not.
+ */
+ public function displayBundle() {
+ // The bundle key is explicitly defined in the entity definition.
+ if (isset($this->baseEntityInfo['bundle_keys']['bundle'])) {
+ return TRUE;
+ }
+
+ // There is more than one bundle defined.
+ if (count($this->baseEntityBundles) > 1) {
+ return TRUE;
+ }
+
+ // The defined bundle ones not match the entity type name.
+ if (!empty($this->baseEntityBundles) && !isset($this->baseEntityBundles[$this->baseEntityType])) {
+ return TRUE;
+ }
+
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function sortRows($a, $b) {
+ return $this->sortRowsMultiple($a, $b, array('bundle', 'label'));
+ }
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationListController.php b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationListController.php
new file mode 100644
index 0000000..c920a43
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationListController.php
@@ -0,0 +1,85 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Controller\ConfigTranslationListController.
+ */
+
+namespace Drupal\config_translation\Controller;
+
+use Drupal\config_translation\ConfigMapperManagerInterface;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+
+/**
+ * Defines the configuration translation list controller.
+ */
+class ConfigTranslationListController extends ControllerBase implements ContainerInjectionInterface {
+
+ /**
+ * Definition of the config mapper.
+ *
+ * @var array
+ */
+ protected $mapperDefinition;
+
+ /**
+ * The config mapper.
+ *
+ * @var \Drupal\config_translation\ConfigEntityMapper
+ */
+ protected $mapper;
+
+ /**
+ * Constructs a new ConfigTranslationListController object.
+ *
+ * @param \Drupal\config_translation\ConfigMapperManagerInterface $mapper_manager
+ * The config mapper manager.
+ * @param $config_translation_mapper
+ * The config mapper id.
+ */
+ public function __construct(ConfigMapperManagerInterface $mapper_manager, $config_translation_mapper) {
+ $this->mapperDefinition = $mapper_manager->getDefinition($config_translation_mapper);
+ $this->mapper = $mapper_manager->createInstance($config_translation_mapper, $this->mapperDefinition);
+ }
+
+ /**
+ * {inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('plugin.manager.config_translation.mapper'),
+ $container->get('request')->attributes->get('_raw_variables')->get('config_translation_mapper')
+ );
+ }
+
+ /**
+ * Provides the listing page for any entity type.
+ *
+ * @return array
+ * A render array as expected by drupal_render().
+ *
+ * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
+ * Throws an exception if a mapper plugin could not be instantiated from the
+ * mapper definition in the constructor.
+ */
+ public function listing() {
+ if (!$this->mapper) {
+ throw new NotFoundHttpException();
+ }
+ $entity_type = $this->mapper->getType();
+ // If the mapper, for example the mapper for field instances, has a custom
+ // list controller defined, use it. Other mappers, for examples the ones for
+ // node_type and block, fallback to the generic configuration translation
+ // list controller.
+ $class = $this->mapperDefinition['list_controller'];
+ /** @var \Drupal\config_translation\Controller\ConfigTranslationEntityListControllerInterface $controller */
+ $controller = new $class($entity_type, $this->entityManager()->getDefinition($entity_type), $this->entityManager()->getStorageController($entity_type), $this->moduleHandler(), $this->entityManager(), $this->mapperDefinition);
+ $build = $controller->render();
+ $build['#title'] = $this->mapper->getTypeLabel();
+ return $build;
+ }
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationMapperList.php b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationMapperList.php
new file mode 100644
index 0000000..c1ef9e5
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Controller/ConfigTranslationMapperList.php
@@ -0,0 +1,137 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Controller\ConfigTranslationMapperList.
+ */
+
+namespace Drupal\config_translation\Controller;
+
+use Drupal\Component\Utility\String;
+use Drupal\config_translation\ConfigMapperInterface;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines the configuration translation mapper list.
+ *
+ * Groups all defined configuration mapper instances by weight.
+ */
+class ConfigTranslationMapperList extends ControllerBase implements ContainerInjectionInterface {
+
+ /**
+ * A array of configuration mapper instances.
+ *
+ * @var \Drupal\config_translation\ConfigMapperInterface[]
+ */
+ protected $mappers;
+
+ /**
+ * Constructs a new ConfigTranslationMapperIndex object.
+ *
+ * @param \Drupal\config_translation\ConfigMapperInterface[] $mappers
+ * The configuration mapper manager.
+ */
+ public function __construct(array $mappers) {
+ $this->mappers = $mappers;
+ }
+
+ /**
+ * {inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('plugin.manager.config_translation.mapper')->getMappers()
+ );
+ }
+
+ /**
+ * Builds the mappers as a renderable array for theme_table().
+ *
+ * @return array
+ * Renderable array with config translation mappers.
+ */
+ public function render() {
+ $build = array(
+ '#theme' => 'table',
+ '#header' => $this->buildHeader(),
+ '#rows' => array(),
+ );
+
+ $mappers = array();
+
+ foreach ($this->mappers as $mapper) {
+ if ($row = $this->buildRow($mapper)) {
+ $mappers[$mapper->getWeight()][] = $row;
+ }
+ }
+
+ // Group by mapper weight and sort by label.
+ ksort($mappers);
+ foreach ($mappers as $weight => $mapper) {
+ usort($mapper, function ($a, $b) {
+ $a_title = (isset($a['label'])) ? $a['label'] : '';
+ $b_title = (isset($b['label'])) ? $b['label'] : '';
+ return strnatcasecmp($a_title, $b_title);
+ });
+ $mappers[$weight] = $mapper;
+ }
+
+ foreach ($mappers as $mapper) {
+ $build['#rows'] = array_merge($build['#rows'], $mapper);
+ }
+
+ return $build;
+ }
+
+ /**
+ * Builds a row for a mapper in the mapper listing.
+ *
+ * @param \Drupal\config_translation\ConfigMapperInterface $mapper
+ * The mapper.
+ *
+ * @return array
+ * A render array structure of fields for this mapper.
+ */
+ public function buildRow(ConfigMapperInterface $mapper) {
+ $row['label'] = String::checkPlain($mapper->getTypeLabel());
+ $row['operations']['data'] = $this->buildOperations($mapper);
+ return $row;
+ }
+
+ /**
+ * Builds the header row for the mapper listing.
+ *
+ * @return array
+ * A render array structure of header strings.
+ */
+ public function buildHeader() {
+ $row['Label'] = $this->t('Label');
+ $row['operations'] = $this->t('Operations');
+ return $row;
+ }
+
+ /**
+ * Builds a renderable list of operation links for the entity.
+ *
+ * @param \Drupal\config_translation\ConfigMapperInterface $mapper
+ * The mapper.
+ *
+ * @return array
+ * A renderable array of operation links.
+ *
+ * @see \Drupal\Core\Entity\EntityListController::buildOperations()
+ */
+ protected function buildOperations(ConfigMapperInterface $mapper) {
+ // Retrieve and sort operations.
+ $operations = $mapper->getOperations();
+ uasort($operations, 'Drupal\Component\Utility\SortArray::sortByWeightElement');
+ $build = array(
+ '#type' => 'operations',
+ '#links' => $operations,
+ );
+ return $build;
+ }
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Exception/InvalidMapperDefinitionException.php b/core/modules/config_translation/lib/Drupal/config_translation/Exception/InvalidMapperDefinitionException.php
new file mode 100644
index 0000000..eb56606
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Exception/InvalidMapperDefinitionException.php
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Exception\InvalidMapperDefinitionException.
+ */
+
+namespace Drupal\config_translation\Exception;
+
+use Drupal\Component\Plugin\Exception\PluginException;
+
+/**
+ * Defines a class for invalid configuration mapper definition exceptions.
+ */
+class InvalidMapperDefinitionException extends PluginException {
+
+ /**
+ * The plugin ID of the mapper.
+ *
+ * @var string
+ */
+ protected $pluginId;
+
+ /**
+ * Constructs a InvalidMapperDefinitionException.
+ *
+ * @param string $plugin_id
+ * The plugin ID of the mapper.
+ *
+ * @see \Exception for the remaining parameters.
+ */
+ public function __construct($plugin_id, $message = '', $code = 0, \Exception $previous = NULL) {
+ $this->pluginId = $plugin_id;
+ parent::__construct($message, $code, $previous);
+ }
+
+ /**
+ * Returns the plugin ID of the mapper that raised the exception.
+ *
+ * @return string
+ * The plugin ID.
+ */
+ public function getPluginId() {
+ return $this->pluginId;
+ }
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationAddForm.php b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationAddForm.php
new file mode 100644
index 0000000..34b9405
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationAddForm.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Form\ConfigTranslationAddForm.
+ */
+
+namespace Drupal\config_translation\Form;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Defines a form controller for adding configuration translations.
+ */
+class ConfigTranslationAddForm extends ConfigTranslationFormBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'config_translation_add_form';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, array &$form_state, Request $request = NULL, $plugin_id = NULL, $langcode = NULL) {
+ $form = parent::buildForm($form, $form_state, $request, $plugin_id, $langcode);
+ $form['#title'] = $this->t('Add @language translation for %label', array(
+ '%label' => $this->mapper->getTitle(),
+ '@language' => $this->language->name,
+ ));
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, array &$form_state) {
+ parent::submitForm($form, $form_state);
+ drupal_set_message($this->t('Successfully saved @language translation.', array('@language' => $this->language->name)));
+ }
+
+}
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
new file mode 100644
index 0000000..ada6cfa
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationDeleteForm.php
@@ -0,0 +1,161 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Form\ConfigTranslationDeleteForm.
+ */
+
+namespace Drupal\config_translation\Form;
+
+use Drupal\config_translation\ConfigMapperInterface;
+use Drupal\config_translation\ConfigMapperManagerInterface;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Config\StorageInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Form\ConfirmFormBase;
+use Drupal\Core\Language\Language;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+
+/**
+ * Builds a form to delete configuration translation.
+ */
+class ConfigTranslationDeleteForm extends ConfirmFormBase {
+
+ /**
+ * The configuration storage.
+ *
+ * @var \Drupal\Core\Config\StorageInterface $config_storage
+ */
+ protected $configStorage;
+
+ /**
+ * The configuration mapper manager.
+ *
+ * @var \Drupal\config_translation\ConfigMapperManagerInterface
+ */
+ protected $configMapperManager;
+
+ /**
+ * The module handler.
+ *
+ * @var \Drupal\Core\Extension\ModuleHandlerInterface
+ */
+ protected $moduleHandler;
+
+ /**
+ * The configuration translation to be deleted.
+ *
+ * @var \Drupal\config_translation\ConfigMapperInterface
+ */
+ protected $mapper;
+
+ /**
+ * The language of configuration translation.
+ *
+ * @var \Drupal\Core\Language\Language
+ */
+ protected $language;
+
+ /**
+ * Constructs a ConfigTranslationDeleteForm.
+ *
+ * @param \Drupal\Core\Config\StorageInterface $config_storage
+ * The configuration storage.
+ * @param \Drupal\config_translation\ConfigMapperManagerInterface $config_mapper_manager
+ * The configuration mapper manager.
+ * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+ * The module handler.
+ */
+ public function __construct(StorageInterface $config_storage, ConfigMapperManagerInterface $config_mapper_manager, ModuleHandlerInterface $module_handler) {
+ $this->configStorage = $config_storage;
+ $this->configMapperManager = $config_mapper_manager;
+ $this->moduleHandler = $module_handler;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('config.storage'),
+ $container->get('plugin.manager.config_translation.mapper'),
+ $container->get('module_handler')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getQuestion() {
+ return $this->t('Are you sure you want to delete the @language translation of %label?', array('%label' => $this->mapper->getTitle(), '@language' => $this->language->name));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfirmText() {
+ return $this->t('Delete');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelRoute() {
+ return array(
+ 'route_name' => $this->mapper->getOverviewRouteName(),
+ 'route_parameters' => $this->mapper->getOverviewRouteParameters(),
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormID() {
+ return 'config_translation_delete_form';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, array &$form_state, Request $request = NULL, $plugin_id = NULL, $langcode = NULL) {
+ /** @var \Drupal\config_translation\ConfigMapperInterface $mapper */
+ $mapper = $this->configMapperManager->createInstance($plugin_id);
+ $mapper->populateFromRequest($request);
+
+ $language = language_load($langcode);
+ if (!$language) {
+ throw new NotFoundHttpException();
+ }
+
+ $this->mapper = $mapper;
+ $this->language = $language;
+ return parent::buildForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, array &$form_state) {
+ foreach ($this->mapper->getConfigNames() as $name) {
+ $this->configStorage->delete('locale.config.' . $this->language->id . '.' . $name);
+ }
+
+ // Flush all persistent caches.
+ $this->moduleHandler->invokeAll('cache_flush');
+ foreach (Cache::getBins() as $service_id => $cache_backend) {
+ if ($service_id != 'cache.menu') {
+ $cache_backend->deleteAll();
+ }
+ }
+
+ drupal_set_message($this->t('@language translation of %label was deleted', array('%label' => $this->mapper->getTitle(), '@language' => $this->language->name)));
+
+ $form_state['redirect_route'] = array(
+ 'route_name' => $this->mapper->getOverviewRoute(),
+ 'route_parameters' => $this->mapper->getOverviewRouteParameters(),
+ );
+ }
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationEditForm.php b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationEditForm.php
new file mode 100644
index 0000000..f6457a9
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationEditForm.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Form\ConfigTranslationEditForm.
+ */
+
+namespace Drupal\config_translation\Form;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Defines a form controller for editing configuration translations.
+ */
+class ConfigTranslationEditForm extends ConfigTranslationFormBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'config_translation_edit_form';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, array &$form_state, Request $request = NULL, $plugin_id = NULL, $langcode = NULL) {
+ $form = parent::buildForm($form, $form_state, $request, $plugin_id, $langcode);
+ $form['#title'] = $this->t('Edit @language translation for %label', array(
+ '%label' => $this->mapper->getTitle(),
+ '@language' => $this->language->name,
+ ));
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, array &$form_state) {
+ parent::submitForm($form, $form_state);
+ drupal_set_message($this->t('Successfully updated @language translation.', array('@language' => $this->language->name)));
+ }
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationFormBase.php b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationFormBase.php
new file mode 100644
index 0000000..48a1000
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationFormBase.php
@@ -0,0 +1,408 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Form\ConfigTranslationFormBase.
+ */
+
+namespace Drupal\config_translation\Form;
+
+use Drupal\config_translation\ConfigMapperInterface;
+use Drupal\config_translation\ConfigMapperManagerInterface;
+use Drupal\Core\Config\Config;
+use Drupal\Core\Config\Schema\Element;
+use Drupal\Core\Config\TypedConfigManager;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Form\BaseFormIdInterface;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Language\Language;
+use Drupal\locale\StringStorageInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+
+/**
+ * Provides a base form for configuration translations.
+ */
+abstract class ConfigTranslationFormBase extends FormBase implements BaseFormIdInterface {
+
+ /**
+ * The typed configuration manager.
+ *
+ * @var \Drupal\Core\Config\TypedConfigManager
+ */
+ protected $typedConfigManager;
+
+ /**
+ * The configuration mapper manager.
+ *
+ * @var \Drupal\config_translation\ConfigMapperManagerInterface
+ */
+ protected $configMapperManager;
+
+ /**
+ * String translation storage object.
+ *
+ * @var \Drupal\locale\StringStorageInterface
+ */
+ protected $localeStorage;
+
+ /**
+ * The module handler to invoke the alter hook.
+ *
+ * @var \Drupal\Core\Extension\ModuleHandlerInterface
+ */
+ protected $moduleHandler;
+
+ /**
+ * The mapper for configuration translation.
+ *
+ * @var \Drupal\config_translation\ConfigMapperInterface
+ */
+ protected $mapper;
+
+ /**
+ * The language of the configuration translation.
+ *
+ * @var \Drupal\Core\Language\Language
+ */
+ protected $language;
+
+ /**
+ * The language of the configuration translation source.
+ *
+ * @var \Drupal\Core\Language\Language
+ */
+ protected $sourceLanguage;
+
+ /**
+ * An array of base language configuration data keyed by configuration names.
+ *
+ * @var array
+ */
+ protected $baseConfigData = array();
+
+ /**
+ * Creates manage form object with string translation storage.
+ *
+ * @param \Drupal\Core\Config\TypedConfigManager $typed_config_manager
+ * The typed configuration manager.
+ * @param \Drupal\config_translation\ConfigMapperManagerInterface $config_mapper_manager
+ * The configuration mapper manager.
+ * @param \Drupal\locale\StringStorageInterface $locale_storage
+ * The translation storage object.
+ * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+ * The module handler to invoke the alter hook.
+ */
+ public function __construct(TypedConfigManager $typed_config_manager, ConfigMapperManagerInterface $config_mapper_manager, StringStorageInterface $locale_storage, ModuleHandlerInterface $module_handler) {
+ $this->typedConfigManager = $typed_config_manager;
+ $this->configMapperManager = $config_mapper_manager;
+ $this->localeStorage = $locale_storage;
+ $this->moduleHandler = $module_handler;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('config.typed'),
+ $container->get('plugin.manager.config_translation.mapper'),
+ $container->get('locale.storage'),
+ $container->get('module_handler')
+ );
+ }
+
+ /**
+ * {@inheritdoc}.
+ */
+ public function getBaseFormID() {
+ return 'config_translation_form';
+ }
+
+ /**
+ * Implements \Drupal\Core\Form\FormInterface::buildForm().
+ *
+ * Builds configuration form with metadata and values from the source
+ * language.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param array $form_state
+ * An associative array containing the current state of the form.
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ * Page request object.
+ * @param string $plugin_id
+ * The plugin ID of the mapper.
+ * @param string $langcode
+ * The language code of the language the form is adding or editing.
+ *
+ * @return array
+ * The form structure.
+ *
+ * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
+ * Throws an exception if the language code provided as a query parameter in
+ * the request does not match an active language.
+ */
+ public function buildForm(array $form, array &$form_state, Request $request = NULL, $plugin_id = NULL, $langcode = NULL) {
+ /** @var \Drupal\config_translation\ConfigMapperInterface $mapper */
+ $mapper = $this->configMapperManager->createInstance($plugin_id);
+ $mapper->populateFromRequest($request);
+
+ $language = language_load($langcode);
+ if (!$language) {
+ throw new NotFoundHttpException();
+ }
+
+ $this->mapper = $mapper;
+ $this->language = $language;
+ $this->sourceLanguage = $this->mapper->getLanguageWithFallback();
+
+ // Make sure we are in the override free configuration context. For example,
+ // visiting the configuration page in another language would make those
+ // language overrides active by default. But we need the original values.
+ config_context_enter('config.context.free');
+ // 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.
+ $this->baseConfigData = $this->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\Core\Config\Context\LanguageConfigContext')->setLanguage($this->language);
+
+ // Add some information to the form state for easier form altering.
+ $form_state['config_translation_mapper'] = $this->mapper;
+ $form_state['config_translation_language'] = $this->language;
+ $form_state['config_translation_source_language'] = $this->sourceLanguage;
+
+ $form['#attached']['library'][] = array('config_translation', 'drupal.config_translation.admin');
+
+ $form['config_names'] = array(
+ '#type' => 'container',
+ '#tree' => TRUE,
+ );
+ foreach ($this->mapper->getConfigNames() as $name) {
+ $form['config_names'][$name] = array('#type' => 'container');
+ $form['config_names'][$name] += $this->buildConfigForm($this->typedConfigManager->get($name), $this->config($name)->get(), $this->baseConfigData[$name]);
+ }
+
+ $form['actions']['#type'] = 'actions';
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => $this->t('Save translation'),
+ '#button_type' => 'primary',
+ );
+
+ // Leave the language context so that configuration accessed later in the
+ // request is displayed in the correct language.
+ config_context_leave();
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, array &$form_state) {
+ $form_values = $form_state['values']['config_names'];
+
+ // For the form submission handling, use the override free context.
+ config_context_enter('config.context.free');
+
+ foreach ($this->mapper->getConfigNames() as $name) {
+ // Set configuration values based on form submission and source values.
+ $base_config = $this->config($name);
+ $translation_config = $this->config('locale.config.' . $this->language->id . '.' . $name);
+ $locations = $this->localeStorage->getLocations(array('type' => 'configuration', 'name' => $name));
+
+ $this->setConfig($this->language, $base_config, $translation_config, $form_values[$name], !empty($locations));
+
+ // If no overrides, delete language specific configuration file.
+ $saved_config = $translation_config->get();
+ if (empty($saved_config)) {
+ $translation_config->delete();
+ }
+ else {
+ $translation_config->save();
+ }
+ }
+
+ config_context_leave();
+
+ $form_state['redirect_route'] = array(
+ 'route_name' => $this->mapper->getOverviewRoute(),
+ 'route_parameters' => $this->mapper->getOverviewRouteParameters(),
+ );
+ }
+
+ /**
+ * Formats configuration schema as a form tree.
+ *
+ * @param \Drupal\Core\Config\Schema\Element $schema
+ * Schema definition of configuration.
+ * @param array|string $config_data
+ * Configuration object of requested language, a string when done traversing
+ * the data building each sub-structure for the form.
+ * @param array|string $base_config_data
+ * Configuration object of base language, a string when done traversing
+ * the data building each sub-structure for the form.
+ * @param bool $collapsed
+ * (optional) Flag to set collapsed state. Defaults to FALSE.
+ * @param string|null $base_key
+ * (optional) Base configuration key. Defaults to an empty string.
+ *
+ * @return array
+ * An associative array containing the structure of the form.
+ */
+ protected function buildConfigForm(Element $schema, $config_data, $base_config_data, $collapsed = FALSE, $base_key = '') {
+ $build = array();
+ foreach ($schema as $key => $element) {
+ // Make the specific element key, "$base_key.$key".
+ $element_key = implode('.', array_filter(array($base_key, $key)));
+ $definition = $element->getDefinition() + array('label' => $this->t('N/A'));
+ if ($element instanceof Element) {
+ // Build sub-structure and include it with a wrapper in the form
+ // if there are any translatable elements there.
+ $sub_build = $this->buildConfigForm($element, $config_data[$key], $base_config_data[$key], TRUE, $element_key);
+ if (!empty($sub_build)) {
+ // For some configuration elements the same element structure can
+ // repeat multiple times, (like views displays, filters, etc.).
+ // So try to find a more usable title for the details summary. First
+ // check if there is an element which is called title or label, then
+ // check if there is an element which contains these words.
+ $title = '';
+ if (isset($sub_build['title']['source'])) {
+ $title = $sub_build['title']['source']['#markup'];
+ }
+ elseif (isset($sub_build['label']['source'])) {
+ $title = $sub_build['label']['source']['#markup'];
+ }
+ else {
+ foreach (array_keys($sub_build) as $title_key) {
+ if (isset($sub_build[$title_key]['source']) && (strpos($title_key, 'title') !== FALSE || strpos($title_key, 'label') !== FALSE)) {
+ $title = $sub_build[$title_key]['source']['#markup'];
+ break;
+ }
+ }
+ }
+ $build[$key] = array(
+ '#type' => 'details',
+ '#title' => (!empty($title) ? (strip_tags($title) . ' ') : '') . $this->t($definition['label']),
+ '#collapsible' => TRUE,
+ '#collapsed' => $collapsed,
+ ) + $sub_build;
+ }
+ }
+ else {
+ $definition = $element->getDefinition();
+
+ // Invoke hook_config_translation_type_info_alter() implementations to
+ // alter the configuration types.
+ $definitions = array(
+ $definition['type'] => &$definition,
+ );
+ $this->moduleHandler->alter('config_translation_type_info', $definitions);
+
+ // Create form element only for translatable items.
+ if (!isset($definition['translatable']) || !isset($definition['type'])) {
+ continue;
+ }
+
+ $value = $config_data[$key];
+ $build[$element_key] = array(
+ '#theme' => 'config_translation_manage_form_element',
+ );
+ $build[$element_key]['source'] = array(
+ '#markup' => $base_config_data[$key] ? ('<span lang="' . $this->sourceLanguage->id . '">' . nl2br($base_config_data[$key] . '</span>')) : t('(Empty)'),
+ '#title' => $this->t(
+ '!label <span class="visually-hidden">(!source_language)</span>',
+ array(
+ '!label' => $this->t($definition['label']),
+ '!source_language' => $this->sourceLanguage->name,
+ )
+ ),
+ '#type' => 'item',
+ );
+
+ $definition += array('form_element_class' => '\Drupal\config_translation\FormElement\Textfield');
+
+ /** @var \Drupal\config_translation\FormElement\ElementInterface $form_element */
+ $form_element = new $definition['form_element_class']();
+ $build[$element_key]['translation'] = $form_element->getFormElement($definition, $this->language, $value);
+ }
+ }
+ return $build;
+ }
+
+ /**
+ * Sets configuration based on a nested form value array.
+ *
+ * @param \Drupal\Core\Language\Language $language
+ * Set the configuration in this language.
+ * @param \Drupal\Core\Config\Config $base_config
+ * Base configuration values, in the source language.
+ * @param \Drupal\Core\Config\Config $translation_config
+ * Translation configuration instance. Values from $config_values will be
+ * set in this instance.
+ * @param array $config_values
+ * A simple one dimensional or recursive array:
+ * - simple:
+ * array(name => array('translation' => 'French site name'));
+ * - recursive:
+ * cancel_confirm => array(
+ * cancel_confirm.subject => array('translation' => 'Subject'),
+ * cancel_confirm.body => array('translation' => 'Body content'),
+ * );
+ * Either format is used, the nested arrays are just containers and not
+ * needed for saving the data.
+ * @param bool $shipped_config
+ * Flag to specify whether the configuration had a shipped version and
+ * therefore should also be stored in the locale database.
+ */
+ protected function setConfig(Language $language, Config $base_config, Config $translation_config, array $config_values, $shipped_config = FALSE) {
+ foreach ($config_values as $key => $value) {
+ if (is_array($value) && !isset($value['translation'])) {
+ // Traverse into this level in the configuration.
+ $this->setConfig($language, $base_config, $translation_config, $value, $shipped_config);
+ }
+ else {
+
+ // If the configuration file being translated was originally shipped, we
+ // should update the locale translation storage. The string should
+ // already be there, but we make sure to check.
+ if ($shipped_config && $source_string = $this->localeStorage->findString(array('source' => $base_config->get($key)))) {
+
+ // Get the translation for this original source string from locale.
+ $conditions = array(
+ 'lid' => $source_string->lid,
+ 'language' => $language->id,
+ );
+ $translations = $this->localeStorage->getTranslations($conditions + array('translated' => TRUE));
+ // If we got a translation, take that, otherwise create a new one.
+ $translation = reset($translations) ?: $this->localeStorage->createTranslation($conditions);
+
+ // If we have a new translation or different from what is stored in
+ // locale before, save this as an updated customize translation.
+ if ($translation->isNew() || $translation->getString() != $value['translation']) {
+ $translation->setString($value['translation'])
+ ->setCustomized()
+ ->save();
+ }
+ }
+
+ // Save value, if different from the source value in the base
+ // configuration. If same as original configuration, remove override.
+ if ($base_config->get($key) !== $value['translation']) {
+ $translation_config->set($key, $value['translation']);
+ }
+ else {
+ $translation_config->clear($key);
+ }
+ }
+ }
+ }
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/FormElement/DateFormat.php b/core/modules/config_translation/lib/Drupal/config_translation/FormElement/DateFormat.php
new file mode 100644
index 0000000..3cc9867
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/FormElement/DateFormat.php
@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\FormElement\DateFormat.
+ */
+
+namespace Drupal\config_translation\FormElement;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\ReplaceCommand;
+use Drupal\Core\Language\Language;
+
+/**
+ * Defines the date format element for the configuration translation interface.
+ */
+class DateFormat extends Element {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormElement(array $definition, Language $language, $value) {
+ if (class_exists('intlDateFormatter')) {
+ $description = $this->t('A user-defined date format. See the <a href="@url">PHP manual</a> for available options.', array('@url' => 'http://userguide.icu-project.org/formatparse/datetime'));
+ }
+ else {
+ $description = $this->t('A user-defined date format. See the <a href="@url">PHP manual</a> for available options.', array('@url' => 'http://php.net/manual/function.date.php'));
+ }
+ $format = $this->t('Displayed as %date_format', array('%date_format' => \Drupal::service('date')->format(REQUEST_TIME, 'custom', $value)));
+ return array(
+ '#type' => 'textfield',
+ '#title' => $this->t($definition['label']) . '<span class="visually-hidden"> (' . $language->name . ')</span>',
+ '#description' => $description,
+ '#default_value' => $value,
+ '#attributes' => array('lang' => $language->id),
+ '#field_suffix' => ' <div class="edit-date-format-suffix"><small id="edit-date-format-suffix">' . $format . '</small></div>',
+ '#ajax' => array(
+ 'callback' => 'Drupal\config_translation\FormElement\DateFormat::ajaxSample',
+ 'event' => 'keyup',
+ 'progress' => array('type' => 'throbber', 'message' => NULL),
+ ),
+ );
+ }
+
+ /**
+ * Ajax callback to render a sample of the input date format.
+ *
+ * @param array $form
+ * Form API array structure.
+ * @param array $form_state
+ * Form state information.
+ *
+ * @return AjaxResponse
+ * Ajax response with the rendered sample date using the given format. If
+ * the given format cannot be identified or was empty, the response will
+ * be empty as well.
+ */
+ public static function ajaxSample(array $form, array $form_state) {
+ $response = new AjaxResponse();
+
+ $format_value = NestedArray::getValue($form_state['values'], $form_state['triggering_element']['#array_parents']);
+ if (!empty($format_value)) {
+ // Format the date with a custom date format with the given pattern.
+ // The object is not instantiated in an Ajax context, so $this->t()
+ // cannot be used here.
+ $format = t('Displayed as %date_format', array('%date_format' => \Drupal::service('date')->format(REQUEST_TIME, 'custom', $format_value)));
+
+ // Return a command instead of a string, since the Ajax framework
+ // automatically prepends an additional empty DIV element for a string,
+ // which breaks the layout.
+ $response->addCommand(new ReplaceCommand('#edit-date-format-suffix', '<small id="edit-date-format-suffix">' . $format . '</small>'));
+ }
+
+ return $response;
+ }
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/FormElement/Element.php b/core/modules/config_translation/lib/Drupal/config_translation/FormElement/Element.php
new file mode 100644
index 0000000..3c55c6d
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/FormElement/Element.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\FormElement\Element.
+ */
+
+namespace Drupal\config_translation\FormElement;
+
+/**
+ * Base class for form elements.
+ */
+abstract class Element implements ElementInterface {
+
+ /**
+ * The translation manager.
+ *
+ * @var \Drupal\Core\StringTranslation\TranslationInterface
+ */
+ protected $translationManager;
+
+ /**
+ * Translates a string to the current language or to a given language.
+ *
+ * See the t() documentation for details.
+ */
+ protected function t($string, array $args = array(), array $options = array()) {
+ return $this->translationManager()->translate($string, $args, $options);
+ }
+
+ /**
+ * Returns the translation manager.
+ *
+ * @return \Drupal\Core\StringTranslation\TranslationInterface
+ * The translation manager.
+ */
+ protected function translationManager() {
+ if (!$this->translationManager) {
+ $this->translationManager = \Drupal::translation();
+ }
+ return $this->translationManager;
+ }
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/FormElement/ElementInterface.php b/core/modules/config_translation/lib/Drupal/config_translation/FormElement/ElementInterface.php
new file mode 100644
index 0000000..9072cfc
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/FormElement/ElementInterface.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\FormElement\ElementInterface.
+ */
+
+namespace Drupal\config_translation\FormElement;
+
+use Drupal\Core\Language\Language;
+
+/**
+ * Provides an interface for configuration translation form elements.
+ */
+interface ElementInterface {
+
+ /**
+ * Returns the translation form element for a given configuration definition.
+ *
+ * @param array $definition
+ * Configuration schema type definition of the element.
+ * @param \Drupal\Core\Language\Language $language
+ * Language object to display the translation form for.
+ * @param string $value
+ * Default value for the form element.
+ *
+ * @return array
+ * Form API array to represent the form element.
+ */
+ public function getFormElement(array $definition, Language $language, $value);
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/FormElement/Textarea.php b/core/modules/config_translation/lib/Drupal/config_translation/FormElement/Textarea.php
new file mode 100644
index 0000000..e79107e
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/FormElement/Textarea.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\FormElement\Textarea.
+ */
+
+namespace Drupal\config_translation\FormElement;
+
+use Drupal\Core\Language\Language;
+
+/**
+ * Defines the textarea element for the configuration translation interface.
+ */
+class Textarea extends Element {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormElement(array $definition, Language $language, $value) {
+ // Estimate a comfortable size of the input textarea.
+ $rows_words = ceil(str_word_count($value) / 5);
+ $rows_newlines = substr_count($value, "\n" ) + 1;
+ $rows = max($rows_words, $rows_newlines);
+
+ return array(
+ '#type' => 'textarea',
+ '#default_value' => $value,
+ '#title' => $this->t($definition['label']) . '<span class="visually-hidden"> (' . $language->name . ')</span>',
+ '#rows' => $rows,
+ '#attributes' => array('lang' => $language->id),
+ );
+ }
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/FormElement/Textfield.php b/core/modules/config_translation/lib/Drupal/config_translation/FormElement/Textfield.php
new file mode 100644
index 0000000..1791961
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/FormElement/Textfield.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\FormElement\Textfield.
+ */
+
+namespace Drupal\config_translation\FormElement;
+
+use Drupal\Core\Language\Language;
+
+/**
+ * Defines the textfield element for the configuration translation interface.
+ */
+class Textfield extends Element {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormElement(array $definition, Language $language, $value) {
+ return array(
+ '#type' => 'textfield',
+ '#default_value' => $value,
+ '#title' => $this->t($definition['label']) . '<span class="visually-hidden"> (' . $language->name . ')</span>',
+ '#attributes' => array('lang' => $language->id),
+ );
+ }
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Plugin/Derivative/ConfigTranslationContextualLinks.php b/core/modules/config_translation/lib/Drupal/config_translation/Plugin/Derivative/ConfigTranslationContextualLinks.php
new file mode 100644
index 0000000..0d6a3a9
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Plugin/Derivative/ConfigTranslationContextualLinks.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Plugin\Derivative\ConfigTranslationContextualLinks.
+ */
+
+namespace Drupal\config_translation\Plugin\Derivative;
+
+use Drupal\Component\Utility\Unicode;
+use Drupal\Component\Plugin\Derivative\DerivativeBase;
+use Drupal\config_translation\ConfigMapperManagerInterface;
+use Drupal\Core\Plugin\Discovery\ContainerDerivativeInterface;
+use Drupal\Core\Routing\RouteProviderInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides dynamic contextual links for configuration translation.
+ */
+class ConfigTranslationContextualLinks extends DerivativeBase implements ContainerDerivativeInterface {
+
+ /**
+ * The mapper plugin discovery service.
+ *
+ * @var \Drupal\config_translation\ConfigMapperManagerInterface
+ */
+ protected $mapperManager;
+
+ /**
+ * Constructs a new ConfigTranslationContextualLinks.
+ *
+ * @param \Drupal\config_translation\ConfigMapperManagerInterface $mapper_manager
+ * The mapper plugin discovery service.
+ */
+ public function __construct(ConfigMapperManagerInterface $mapper_manager) {
+ $this->mapperManager = $mapper_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, $base_plugin_id) {
+ return new static(
+ $container->get('plugin.manager.config_translation.mapper')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDerivativeDefinitions(array $base_plugin_definition) {
+ // Create contextual links for all mappers.
+ $mappers = $this->mapperManager->getMappers();
+ foreach ($mappers as $plugin_id => $mapper) {
+ // @todo Contextual groups do not map to entity types in a predictable
+ // way. See https://drupal.org/node/2134841 to make them predictable.
+ $group_name = $mapper->getContextualLinkGroup();
+ if (empty($group_name)) {
+ continue;
+ }
+
+ /** @var \Drupal\config_translation\ConfigMapperInterface $mapper */
+ $route_name = $mapper->getOverviewRouteName();
+ $this->derivatives[$route_name] = $base_plugin_definition;
+ $this->derivatives[$route_name]['config_translation_plugin_id'] = $plugin_id;
+ $this->derivatives[$route_name]['class'] = '\Drupal\config_translation\Plugin\Menu\ContextualLink\ConfigTranslationContextualLink';
+ $this->derivatives[$route_name]['route_name'] = $route_name;
+ $this->derivatives[$route_name]['group'] = $group_name;
+ }
+ return parent::getDerivativeDefinitions($base_plugin_definition);
+ }
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Plugin/Derivative/ConfigTranslationLocalTasks.php b/core/modules/config_translation/lib/Drupal/config_translation/Plugin/Derivative/ConfigTranslationLocalTasks.php
new file mode 100644
index 0000000..a662fa7
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Plugin/Derivative/ConfigTranslationLocalTasks.php
@@ -0,0 +1,116 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Plugin\Derivative\ConfigTranslationLocalTasks.
+ */
+
+namespace Drupal\config_translation\Plugin\Derivative;
+
+use Drupal\config_translation\ConfigMapperManagerInterface;
+use Drupal\Component\Plugin\Derivative\DerivativeBase;
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Plugin\Discovery\ContainerDerivativeInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides dynamic local tasks for config translation.
+ */
+class ConfigTranslationLocalTasks extends DerivativeBase implements ContainerDerivativeInterface {
+
+ /**
+ * The mapper plugin discovery service.
+ *
+ * @var \Drupal\config_translation\ConfigMapperManagerInterface
+ */
+ protected $mapperManager;
+
+ /**
+ * The base plugin ID
+ *
+ * @var string
+ */
+ protected $basePluginId;
+
+ /**
+ * Constructs a new ConfigTranslationLocalTasks.
+ *
+ * @param string $base_plugin_id
+ * The base plugin ID.
+ * @param \Drupal\config_translation\ConfigMapperManagerInterface $mapper_manager
+ * The mapper plugin discovery service.
+ */
+ public function __construct($base_plugin_id, ConfigMapperManagerInterface $mapper_manager) {
+ $this->basePluginId = $base_plugin_id;
+ $this->mapperManager = $mapper_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, $base_plugin_id) {
+ return new static(
+ $base_plugin_id,
+ $container->get('plugin.manager.config_translation.mapper')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDerivativeDefinitions(array $base_plugin_definition) {
+ $mappers = $this->mapperManager->getMappers();
+ foreach ($mappers as $plugin_id => $mapper) {
+ /** @var \Drupal\config_translation\ConfigMapperInterface $mapper */
+ $route_name = $mapper->getOverviewRouteName();
+ $this->derivatives[$route_name] = $base_plugin_definition;
+ $this->derivatives[$route_name]['config_translation_plugin_id'] = $plugin_id;
+ $this->derivatives[$route_name]['class'] = '\Drupal\config_translation\Plugin\Menu\LocalTask\ConfigTranslationLocalTask';
+ $this->derivatives[$route_name]['route_name'] = $route_name;
+ }
+ return parent::getDerivativeDefinitions($base_plugin_definition);
+ }
+
+ /**
+ * Alters the local tasks to find the proper tab_root_id for each task.
+ */
+ public function alterLocalTasks(array &$local_tasks) {
+ $mappers = $this->mapperManager->getMappers();
+ foreach ($mappers as $mapper) {
+ /** @var \Drupal\config_translation\ConfigMapperInterface $mapper */
+ $route_name = $mapper->getOverviewRouteName();
+ $translation_tab = $this->basePluginId . ':' . $route_name;
+ $tab_root_id = $this->getTaskFromRoute($mapper->getBaseRouteName(), $local_tasks);
+ if (!empty($tab_root_id)) {
+ $local_tasks[$translation_tab]['tab_root_id'] = $tab_root_id;
+ }
+ else {
+ unset($local_tasks[$translation_tab]);
+ }
+ }
+ }
+
+ /**
+ * Find the local task ID of the parent route given the route name.
+ *
+ * @param string $route_name
+ * The route name of the parent local task.
+ * @param array $local_tasks
+ * An array of all local task definitions.
+ *
+ * @return bool|string
+ * Returns the local task ID of the parent task, otherwise return FALSE.
+ */
+ protected function getTaskFromRoute($route_name, &$local_tasks) {
+ $root_local_task = FALSE;
+ foreach ($local_tasks as $plugin_id => $local_task) {
+ if ($local_task['route_name'] == $route_name) {
+ $root_local_task = $plugin_id;
+ break;
+ }
+ }
+
+ return $root_local_task;
+ }
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Plugin/Menu/ContextualLink/ConfigTranslationContextualLink.php b/core/modules/config_translation/lib/Drupal/config_translation/Plugin/Menu/ContextualLink/ConfigTranslationContextualLink.php
new file mode 100644
index 0000000..98ffb0e
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Plugin/Menu/ContextualLink/ConfigTranslationContextualLink.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Plugin\Menu\ContextualLink\ConfigTranslationContextualLink.
+ */
+
+namespace Drupal\config_translation\Plugin\Menu\ContextualLink;
+
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Menu\ContextualLinkDefault;
+
+/**
+ * Defines a contextual link plugin with a dynamic title.
+ */
+class ConfigTranslationContextualLink extends ContextualLinkDefault {
+
+ /**
+ * The mapper plugin discovery service.
+ *
+ * @var \Drupal\config_translation\ConfigMapperManagerInterface
+ */
+ protected $mapperManager;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTitle() {
+ $options = array();
+ if (!empty($this->pluginDefinition['title_context'])) {
+ $options['context'] = $this->pluginDefinition['title_context'];
+ }
+
+ // Take custom 'config_translation_plugin_id' plugin definition key to
+ // retrieve title. We need to retrieve a runtime title (as opposed to
+ // storing the title on the plugin definition for the link) because
+ // it contains translated parts that we need in the runtime language.
+ $type_name = Unicode::strtolower($this->mapperManager()->createInstance($this->pluginDefinition['config_translation_plugin_id'])->getTypeLabel());
+ return $this->t($this->pluginDefinition['title'], array('@type_name' => $type_name), $options);
+ }
+
+ /**
+ * Gets the mapper manager.
+ *
+ * @return \Drupal\config_translation\ConfigMapperManagerInterface
+ * The mapper manager.
+ */
+ protected function mapperManager() {
+ if (!$this->mapperManager) {
+ $this->mapperManager = \Drupal::service('plugin.manager.config_translation.mapper');
+ }
+ return $this->mapperManager;
+ }
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Plugin/Menu/LocalTask/ConfigTranslationLocalTask.php b/core/modules/config_translation/lib/Drupal/config_translation/Plugin/Menu/LocalTask/ConfigTranslationLocalTask.php
new file mode 100644
index 0000000..46a2ec2
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Plugin/Menu/LocalTask/ConfigTranslationLocalTask.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Plugin\Menu\LocalTask\ConfigTranslationLocalTask.
+ */
+
+namespace Drupal\config_translation\Plugin\Menu\LocalTask;
+
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Menu\LocalTaskDefault;
+
+/**
+ * Defines a local task plugin with a dynamic title.
+ */
+class ConfigTranslationLocalTask extends LocalTaskDefault {
+
+ /**
+ * The mapper plugin discovery service.
+ *
+ * @var \Drupal\config_translation\ConfigMapperManagerInterface
+ */
+ protected $mapperManager;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTitle() {
+ $options = array();
+ if (!empty($this->pluginDefinition['title_context'])) {
+ $options['context'] = $this->pluginDefinition['title_context'];
+ }
+
+ // Take custom 'config_translation_plugin_id' plugin definition key to
+ // retrieve title. We need to retrieve a runtime title (as opposed to
+ // storing the title on the plugin definition for the link) because
+ // it contains translated parts that we need in the runtime language.
+ $type_name = Unicode::strtolower($this->mapperManager()->createInstance($this->pluginDefinition['config_translation_plugin_id'])->getTypeLabel());
+ return $this->t($this->pluginDefinition['title'], array('@type_name' => $type_name), $options);
+ }
+
+ /**
+ * Gets the mapper manager.
+ *
+ * @return \Drupal\config_translation\ConfigMapperManagerInterface
+ * The mapper manager.
+ */
+ protected function mapperManager() {
+ if (!$this->mapperManager) {
+ $this->mapperManager = \Drupal::service('plugin.manager.config_translation.mapper');
+ }
+ return $this->mapperManager;
+ }
+
+}
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
new file mode 100644
index 0000000..ea4b406
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Routing/RouteSubscriber.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Routing\RouteSubscriber.
+ */
+
+namespace Drupal\config_translation\Routing;
+
+use Drupal\Core\Routing\RouteSubscriberBase;
+use Drupal\config_translation\ConfigMapperManagerInterface;
+use Symfony\Component\Routing\RouteCollection;
+
+/**
+ * Listens to the dynamic route events.
+ */
+class RouteSubscriber extends RouteSubscriberBase {
+
+ /**
+ * The mapper plugin discovery service.
+ *
+ * @var \Drupal\config_translation\ConfigMapperManagerInterface
+ */
+ protected $mapperManager;
+
+ /**
+ * Constructs a new RouteSubscriber.
+ *
+ * @param \Drupal\config_translation\ConfigMapperManagerInterface $mapper_manager
+ * The mapper plugin discovery service.
+ */
+ public function __construct(ConfigMapperManagerInterface $mapper_manager) {
+ $this->mapperManager = $mapper_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function routes(RouteCollection $collection) {
+ $mappers = $this->mapperManager->getMappers();
+ foreach ($mappers as $mapper) {
+ $collection->add($mapper->getOverviewRouteName(), $mapper->getOverviewRoute());
+ $collection->add($mapper->getAddRouteName(), $mapper->getAddRoute());
+ $collection->add($mapper->getEditRouteName(), $mapper->getEditRoute());
+ $collection->add($mapper->getDeleteRouteName(), $mapper->getDeleteRoute());
+ }
+ }
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationFormTest.php b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationFormTest.php
new file mode 100644
index 0000000..c5bce3f
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationFormTest.php
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Tests\ConfigTranslationUiTest.
+ */
+
+namespace Drupal\config_translation\Tests;
+
+use Drupal\Core\Language\Language;
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests for altering configuration translation forms.
+ */
+class ConfigTranslationFormTest extends WebTestBase {
+
+ /**
+ * Modules to enable.
+ *
+ * @var array
+ */
+ public static $modules = array('config_translation', 'config_translation_test');
+
+ /**
+ * The plugin ID of the mapper to test.
+ *
+ * @var string
+ */
+ protected $pluginId;
+
+ /**
+ * The language code of the language to use for testing.
+ *
+ * @var string
+ */
+ protected $langcode;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Configuration Translation forms',
+ 'description' => 'Test form altering of configuration translation forms',
+ 'group' => 'Configuration Translation',
+ );
+ }
+
+ public function setUp() {
+ parent::setUp();
+
+ $definitions = \Drupal::service('plugin.manager.config_translation.mapper')->getDefinitions();
+ $this->pluginId = key($definitions);
+
+ $this->langcode = 'xx';
+ $language = new Language(array('id' => $this->langcode, 'name' => 'XX'));
+ language_save($language);
+
+ \Drupal::state()->set('config_translation_test_alter_form_alter', TRUE);
+ }
+
+ /**
+ * Tests altering of the configuration translation forms.
+ */
+ public function testConfigTranslationFormAlter() {
+ $form_builder = \Drupal::formBuilder();
+ $add_form = $form_builder->getForm('Drupal\config_translation\Form\ConfigTranslationAddForm', \Drupal::request(), $this->pluginId, $this->langcode);
+ $edit_form = $form_builder->getForm('Drupal\config_translation\Form\ConfigTranslationEditForm', \Drupal::request(), $this->pluginId, $this->langcode);
+
+ // Test that hook_form_BASE_FORM_ID_alter() was called for the base form ID
+ // 'config_translation_form'.
+ $this->assertTrue($add_form['#base_altered']);
+ $this->assertTrue($edit_form['#base_altered']);
+
+ // Test that hook_form_FORM_ID_alter() was called for the form IDs
+ // 'config_translation_add_form' and 'config_translation_edit_form'.
+ $this->assertTrue($add_form['#altered']);
+ $this->assertTrue($edit_form['#altered']);
+ }
+
+}
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
new file mode 100644
index 0000000..023687c
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationListUiTest.php
@@ -0,0 +1,487 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Tests\ConfigTranslationListUiTest.
+ */
+
+namespace Drupal\config_translation\Tests;
+
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Language\Language;
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests for listings that should have translate operation, except views.
+ *
+ * @see \Drupal\config_translation\Tests\ConfigTranslationViewListUiTest
+ */
+class ConfigTranslationListUiTest extends WebTestBase {
+
+ /**
+ * Modules to enable.
+ *
+ * @var array
+ */
+ public static $modules = array(
+ 'block',
+ 'config_translation',
+ 'contact',
+ 'custom_block',
+ 'field',
+ 'field_ui',
+ 'menu',
+ 'node',
+ 'shortcut',
+ 'taxonomy',
+ 'image',
+ 'picture',
+ 'toolbar',
+ );
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Configuration Translation lists',
+ 'description' => 'Visit all lists.',
+ 'group' => 'Configuration Translation',
+ );
+ }
+
+ /**
+ * Admin user with all needed permissions.
+ *
+ * @var \Drupal\user\Entity\User
+ */
+ protected $adminUser;
+
+ public function setUp() {
+ parent::setUp();
+
+ $permissions = array(
+ 'access site-wide contact form',
+ 'administer blocks',
+ 'administer contact forms',
+ 'administer content types',
+ 'administer custom_block fields',
+ 'administer filters',
+ 'administer menu',
+ 'administer node fields',
+ 'administer permissions',
+ 'administer shortcuts',
+ 'administer site configuration',
+ 'administer taxonomy',
+ 'administer account settings',
+ 'administer languages',
+ 'administer image styles',
+ 'administer pictures',
+ 'translate configuration',
+ );
+
+ // Create and log in user.
+ $this->adminUser = $this->drupalCreateUser($permissions);
+ $this->drupalLogin($this->adminUser);
+ }
+
+ /**
+ * Tests the block listing for the translate operation.
+ *
+ * There are no blocks placed in the testing profile. Add one, then check
+ * for Translate operation.
+ */
+ protected function doBlockListTest() {
+ // Add a test block, any block will do.
+ // Set the machine name so the translate link can be built later.
+ $id = Unicode::strtolower($this->randomName(16));
+ $this->drupalPlaceBlock('system_powered_by_block', array('id' => $id));
+
+ // Get the Block listing.
+ $this->drupalGet('admin/structure/block');
+
+ $translate_link = 'admin/structure/block/manage/' . $id . '/translate';
+ // Test if the link to translate the block is on the page.
+ $this->assertLinkByHref($translate_link);
+
+ // Test if the link to translate actually goes to the translate page.
+ $this->drupalGet($translate_link);
+ $this->assertRaw('<th>' . t('Language') . '</th>');
+ }
+
+ /**
+ * Tests the menu listing for the translate operation.
+ */
+ protected function doMenuListTest() {
+ // Create a test menu to decouple looking for translate operations link so
+ // this does not test more than necessary.
+ $this->drupalGet('admin/structure/menu/add');
+ // Lowercase the machine name.
+ $menu_name = Unicode::strtolower($this->randomName(16));
+ $label = $this->randomName(16);
+ $edit = array(
+ 'id' => $menu_name,
+ 'description' => '',
+ 'label' => $label,
+ );
+ // Create the menu by posting the form.
+ $this->drupalPostForm('admin/structure/menu/add', $edit, t('Save'));
+
+ // Get the Menu listing.
+ $this->drupalGet('admin/structure/menu');
+
+ $translate_link = 'admin/structure/menu/manage/' . $menu_name . '/translate';
+ // Test if the link to translate the menu is on the page.
+ $this->assertLinkByHref($translate_link);
+
+ // Check if the Link is not added if you are missing 'translate
+ // configuration' permission.
+ $permissions = array(
+ 'administer menu',
+ );
+ $this->drupalLogin($this->drupalCreateUser($permissions));
+
+ // Get the Menu listing.
+ $this->drupalGet('admin/structure/menu');
+
+ $translate_link = 'admin/structure/menu/manage/' . $menu_name . '/translate';
+ // Test if the link to translate the menu is NOT on the page.
+ $this->assertNoLinkByHref($translate_link);
+
+ // Login as Admin again otherwise the rest will fail.
+ $this->drupalLogin($this->adminUser);
+
+ // Test if the link to translate actually goes to the translate page.
+ $this->drupalGet($translate_link);
+ $this->assertRaw('<th>' . t('Language') . '</th>');
+ }
+
+ /**
+ * Tests the vocabulary listing for the translate operation.
+ */
+ protected function doVocabularyListTest() {
+ // Create a test vocabulary to decouple looking for translate operations
+ // link so this does not test more than necessary.
+ $vocabulary = entity_create('taxonomy_vocabulary', array(
+ 'name' => $this->randomName(),
+ 'description' => $this->randomName(),
+ 'vid' => Unicode::strtolower($this->randomName()),
+ ));
+ $vocabulary->save();
+
+ // Get the Taxonomy listing.
+ $this->drupalGet('admin/structure/taxonomy');
+
+ $translate_link = 'admin/structure/taxonomy/manage/' . $vocabulary->id() . '/translate';
+ // Test if the link to translate the vocabulary is on the page.
+ $this->assertLinkByHref($translate_link);
+
+ // Test if the link to translate actually goes to the translate page.
+ $this->drupalGet($translate_link);
+ $this->assertRaw('<th>' . t('Language') . '</th>');
+ }
+
+ /**
+ * Tests the custom block listing for the translate operation.
+ */
+ public function doCustomBlockTypeListTest() {
+ // Create a test custom block type to decouple looking for translate
+ // operations link so this does not test more than necessary.
+ $custom_block_type = entity_create('custom_block_type', array(
+ 'id' => Unicode::strtolower($this->randomName(16)),
+ 'label' => $this->randomName(),
+ 'revision' => FALSE
+ ));
+ $custom_block_type->save();
+
+ // Get the custom block type listing.
+ $this->drupalGet('admin/structure/block/custom-blocks/types');
+
+ $translate_link = 'admin/structure/block/custom-blocks/manage/' . $custom_block_type->id() . '/translate';
+ // Test if the link to translate the custom block type is on the page.
+ $this->assertLinkByHref($translate_link);
+
+ // Test if the link to translate actually goes to the translate page.
+ $this->drupalGet($translate_link);
+ $this->assertRaw('<th>' . t('Language') . '</th>');
+ }
+
+ /**
+ * Tests the contact forms listing for the translate operation.
+ */
+ public function doContactFormsListTest() {
+ // Create a test contact form to decouple looking for translate operations
+ // link so this does not test more than necessary.
+ $contact_form = entity_create('contact_category', array(
+ 'id' => Unicode::strtolower($this->randomName(16)),
+ 'label' => $this->randomName(),
+ ));
+ $contact_form->save();
+
+ // Get the contact form listing.
+ $this->drupalGet('admin/structure/contact');
+
+ $translate_link = 'admin/structure/contact/manage/' . $contact_form->id() . '/translate';
+ // Test if the link to translate the contact form is on the page.
+ $this->assertLinkByHref($translate_link);
+
+ // Test if the link to translate actually goes to the translate page.
+ $this->drupalGet($translate_link);
+ $this->assertRaw('<th>' . t('Language') . '</th>');
+ }
+
+ /**
+ * Tests the content type listing for the translate operation.
+ */
+ public function doContentTypeListTest() {
+ // Create a test content type to decouple looking for translate operations
+ // link so this does not test more than necessary.
+ $content_type = entity_create('node_type', array(
+ 'type' => Unicode::strtolower($this->randomName(16)),
+ 'name' => $this->randomName(),
+ ));
+ $content_type->save();
+
+ // Get the content type listing.
+ $this->drupalGet('admin/structure/types');
+
+ $translate_link = 'admin/structure/types/manage/' . $content_type->id() . '/translate';
+ // Test if the link to translate the content type is on the page.
+ $this->assertLinkByHref($translate_link);
+
+ // Test if the link to translate actually goes to the translate page.
+ $this->drupalGet($translate_link);
+ $this->assertRaw('<th>' . t('Language') . '</th>');
+ }
+
+ /**
+ * Tests the formats listing for the translate operation.
+ */
+ public function doFormatsListTest() {
+ // Create a test format to decouple looking for translate operations
+ // link so this does not test more than necessary.
+ $filter_format = entity_create('filter_format', array(
+ 'format' => Unicode::strtolower($this->randomName(16)),
+ 'name' => $this->randomName(),
+ ));
+ $filter_format->save();
+
+ // Get the format listing.
+ $this->drupalGet('admin/config/content/formats');
+
+ $translate_link = 'admin/config/content/formats/manage/' . $filter_format->id() . '/translate';
+ // Test if the link to translate the format is on the page.
+ $this->assertLinkByHref($translate_link);
+
+ // Test if the link to translate actually goes to the translate page.
+ $this->drupalGet($translate_link);
+ $this->assertRaw('<th>' . t('Language') . '</th>');
+ }
+
+ /**
+ * Tests the shortcut listing for the translate operation.
+ */
+ public function doShortcutListTest() {
+ // Create a test shortcut to decouple looking for translate operations
+ // link so this does not test more than necessary.
+ $shortcut = entity_create('shortcut_set', array(
+ 'id' => Unicode::strtolower($this->randomName(16)),
+ 'label' => $this->randomString(),
+ ));
+ $shortcut->save();
+
+ // Get the shortcut listing.
+ $this->drupalGet('admin/config/user-interface/shortcut');
+
+ $translate_link = 'admin/config/user-interface/shortcut/manage/' . $shortcut->id() . '/translate';
+ // Test if the link to translate the shortcut is on the page.
+ $this->assertLinkByHref($translate_link);
+
+ // Test if the link to translate actually goes to the translate page.
+ $this->drupalGet($translate_link);
+ $this->assertRaw('<th>' . t('Language') . '</th>');
+ }
+
+ /**
+ * Tests the role listing for the translate operation.
+ */
+ public function doUserRoleListTest() {
+ // Create a test role to decouple looking for translate operations
+ // link so this does not test more than necessary.
+ $role_id = Unicode::strtolower($this->randomName(16));
+ $this->drupalCreateRole(array(), $role_id);
+
+ // Get the role listing.
+ $this->drupalGet('admin/people/roles');
+
+ $translate_link = 'admin/people/roles/manage/' . $role_id . '/translate';
+ // Test if the link to translate the role is on the page.
+ $this->assertLinkByHref($translate_link);
+
+ // Test if the link to translate actually goes to the translate page.
+ $this->drupalGet($translate_link);
+ $this->assertRaw('<th>' . t('Language') . '</th>');
+ }
+
+ /**
+ * Tests the language listing for the translate operation.
+ */
+ public function doLanguageListTest() {
+ // Create a test language to decouple looking for translate operations
+ // link so this does not test more than necessary.
+ $language = new Language(array('id' => 'ga', 'name' => 'Irish'));
+ language_save($language);
+
+ // Get the language listing.
+ $this->drupalGet('admin/config/regional/language');
+
+ $translate_link = 'admin/config/regional/language/edit/ga/translate';
+ // Test if the link to translate the language is on the page.
+ $this->assertLinkByHref($translate_link);
+
+ // Test if the link to translate actually goes to the translate page.
+ $this->drupalGet($translate_link);
+ $this->assertRaw('<th>' . t('Language') . '</th>');
+ }
+
+ /**
+ * Tests the image style listing for the translate operation.
+ */
+ public function doImageStyleListTest() {
+ // Get the image style listing.
+ $this->drupalGet('admin/config/media/image-styles');
+
+ $translate_link = 'admin/config/media/image-styles/manage/medium/translate';
+ // Test if the link to translate the style is on the page.
+ $this->assertLinkByHref($translate_link);
+
+ // Test if the link to translate actually goes to the translate page.
+ $this->drupalGet($translate_link);
+ $this->assertRaw('<th>' . t('Language') . '</th>');
+ }
+
+ /**
+ * Tests the picture mapping listing for the translate operation.
+ */
+ public function doPictureListTest() {
+ $edit = array();
+ $edit['label'] = $this->randomName();
+ $edit['id'] = strtolower($edit['label']);
+
+ $this->drupalPostForm('admin/config/media/picturemapping/add', $edit, t('Save'));
+ $this->assertRaw(t('Picture mapping %label saved.', array('%label' => $edit['label'])));
+
+ // Get the picture mapping listing.
+ $this->drupalGet('admin/config/media/picturemapping');
+
+ $translate_link = 'admin/config/media/picturemapping/' . $edit['id'] . '/translate';
+ // Test if the link to translate the style is on the page.
+ $this->assertLinkByHref($translate_link);
+
+ // Test if the link to translate actually goes to the translate page.
+ $this->drupalGet($translate_link);
+ $this->assertRaw('<th>' . t('Language') . '</th>');
+ }
+
+ /**
+ * Tests the field listing for the translate operation.
+ */
+ public function doFieldListTest() {
+ // Create a base content type.
+ $content_type = entity_create('node_type', array(
+ 'type' => Unicode::strtolower($this->randomName(16)),
+ 'name' => $this->randomName(),
+ ));
+ $content_type->save();
+
+ // Look at a few fields on a few entity types.
+ $pages = array(
+ array(
+ 'list' => 'admin/structure/types/manage/' . $content_type->id() . '/fields',
+ 'field' => 'node.' . $content_type->id() . '.body',
+ ),
+ array(
+ 'list' => 'admin/structure/block/custom-blocks/manage/basic/fields',
+ 'field' => 'custom_block.basic.body',
+ ),
+ );
+
+ foreach ($pages as $values) {
+ // Get fields listing.
+ $this->drupalGet($values['list']);
+
+ $translate_link = $values['list'] . '/' . $values['field'] . '/translate';
+ // Test if the link to translate the field is on the page.
+ $this->assertLinkByHref($translate_link);
+
+ // Test if the link to translate actually goes to the translate page.
+ $this->drupalGet($translate_link);
+ $this->assertRaw('<th>' . t('Language') . '</th>');
+ }
+ }
+
+ /**
+ * Tests the date format listing for the translate operation.
+ */
+ public function doDateFormatListTest() {
+ // Get the date format listing.
+ $this->drupalGet('admin/config/regional/date-time');
+
+ $translate_link = 'admin/config/regional/date-time/formats/manage/long/translate';
+ // Test if the link to translate the format is on the page.
+ $this->assertLinkByHref($translate_link);
+
+ // Test if the link to translate actually goes to the translate page.
+ $this->drupalGet($translate_link);
+ $this->assertRaw('<th>' . t('Language') . '</th>');
+ }
+
+ /**
+ * Tests a given settings page for the translate operation.
+ *
+ * @param string $link
+ * URL of the settings page to test.
+ */
+ public function doSettingsPageTest($link) {
+ // Get the settings page.
+ $this->drupalGet($link);
+
+ $translate_link = $link . '/translate';
+ // Test if the link to translate the settings page is present.
+ $this->assertLinkByHref($translate_link);
+
+ // Test if the link to translate actually goes to the translate page.
+ $this->drupalGet($translate_link);
+ $this->assertRaw('<th>' . t('Language') . '</th>');
+ }
+
+ /**
+ * Tests if translate link is added to operations in all configuration lists.
+ */
+ public function testTranslateOperationInListUi() {
+ // All lists based on paths provided by the module.
+ $this->doBlockListTest();
+ $this->doMenuListTest();
+ $this->doVocabularyListTest();
+ $this->doCustomBlockTypeListTest();
+ $this->doContactFormsListTest();
+ $this->doContentTypeListTest();
+ $this->doFormatsListTest();
+ $this->doShortcutListTest();
+ $this->doUserRoleListTest();
+ $this->doLanguageListTest();
+ $this->doImageStyleListTest();
+ $this->doPictureListTest();
+ $this->doDateFormatListTest();
+ $this->doFieldListTest();
+
+ // Views is tested in Drupal\config_translation\Tests\ConfigTranslationViewListUiTest
+
+ // Test the maintenance settings page.
+ $this->doSettingsPageTest('admin/config/development/maintenance');
+ // Test the site information settings page.
+ $this->doSettingsPageTest('admin/config/system/site-information');
+ // Test the account settings page.
+ $this->doSettingsPageTest('admin/config/people/accounts');
+ // Test the RSS settings page.
+ $this->doSettingsPageTest('admin/config/services/rss-publishing');
+ }
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationOverviewTest.php b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationOverviewTest.php
new file mode 100644
index 0000000..9ed4a20
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationOverviewTest.php
@@ -0,0 +1,131 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Tests\ConfigTranslationOverviewTest.
+ */
+
+namespace Drupal\config_translation\Tests;
+
+use Drupal\Component\Utility\String;
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Language\Language;
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Functional tests for the Configuration Translation pages.
+ */
+class ConfigTranslationOverviewTest extends WebTestBase {
+
+ /**
+ * Modules to enable.
+ *
+ * @var array
+ */
+ public static $modules = array('contact', 'config_translation', 'views', 'views_ui', 'contextual', 'config_test', 'config_translation_test');
+
+ /**
+ * Languages to enable.
+ *
+ * @var array
+ */
+ protected $langcodes = array('fr', 'ta');
+
+ /**
+ * String translation storage object.
+ *
+ * @var \Drupal\locale\StringStorageInterface
+ */
+ protected $localeStorage;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Configuration Translation Overview',
+ 'description' => 'Translate settings and entities to various languages',
+ 'group' => 'Configuration Translation',
+ );
+ }
+
+ public function setUp() {
+ parent::setUp();
+ $permissions = array(
+ 'translate configuration',
+ 'administer languages',
+ 'administer site configuration',
+ 'administer contact forms',
+ 'access site-wide contact form',
+ 'access contextual links',
+ 'administer views',
+ );
+ // Create and login user.
+ $this->drupalLogin($this->drupalCreateUser($permissions));
+
+ // Add languages.
+ foreach ($this->langcodes as $langcode) {
+ $language = new Language(array('id' => $langcode));
+ language_save($language);
+ }
+ $this->localeStorage = $this->container->get('locale.storage');
+ }
+
+ /**
+ * Tests the config translation mapper page.
+ */
+ public function testMapperListPage() {
+ $this->drupalGet('admin/config/regional/config-translation');
+ $this->assertLinkByHref('admin/config/regional/config-translation/config_test');
+ $this->assertLinkByHref('admin/config/people/accounts/translate');
+
+ $labels = array(
+ '&$nxd~i0',
+ 'some "label" with quotes',
+ $this->randomString(),
+ );
+
+ foreach ($labels as $label) {
+ $test_entity = entity_create('config_test', array(
+ 'id' => $this->randomName(),
+ 'label' => $label,
+ ));
+ $test_entity->save();
+
+ $base_url = 'admin/structure/config_test/manage/' . $test_entity->id();
+ $this->drupalGet('admin/config/regional/config-translation/config_test');
+ $this->assertLinkByHref($base_url . '/translate');
+ $this->assertText(String::checkPlain($test_entity->label()));
+
+ $entity_info = \Drupal::entityManager()->getDefinition($test_entity->entityType());
+ $this->drupalGet($base_url . '/translate');
+
+ $title = t('!label !entity_type', array('!label' => $test_entity->label(), '!entity_type' => Unicode::strtolower($entity_info['label'])));
+ $title = t('Translations for %label', array('%label' => $title));
+ $this->assertRaw($title);
+ $this->assertRaw('<th>' . t('Language') . '</th>');
+
+ $this->drupalGet($base_url);
+ $this->assertLink(t('Translate @title', array('@title' => Unicode::strtolower($entity_info['label']))));
+ }
+ }
+
+ /**
+ * Tests availability of hidden entities in the translation overview.
+ */
+ public function testHiddenEntities() {
+ // Hidden languages are only available to translate through the
+ // configuration translation listings.
+ $this->drupalGet('admin/config/regional/config-translation/language_entity');
+ $this->assertText('Not applicable');
+ $this->assertLinkByHref('admin/config/regional/language/edit/zxx/translate');
+ $this->assertText('Not specified');
+ $this->assertLinkByHref('admin/config/regional/language/edit/und/translate');
+
+ // Hidden date formats are only available to translate through the
+ // configuration translation listings. Test a couple of them.
+ $this->drupalGet('admin/config/regional/config-translation/date_format');
+ $this->assertText('HTML Date');
+ $this->assertLinkByHref('admin/config/regional/date-time/formats/manage/html_date/translate');
+ $this->assertText('HTML Year');
+ $this->assertLinkByHref('admin/config/regional/date-time/formats/manage/html_year/translate');
+ }
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUiTest.php b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUiTest.php
new file mode 100644
index 0000000..dfda109
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationUiTest.php
@@ -0,0 +1,741 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Tests\ConfigTranslationUiTest.
+ */
+
+namespace Drupal\config_translation\Tests;
+
+use Drupal\Component\Utility\Json;
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Config\FileStorage;
+use Drupal\Core\Language\Language;
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Functional tests for the Language list configuration forms.
+ */
+class ConfigTranslationUiTest extends WebTestBase {
+
+ /**
+ * Modules to enable.
+ *
+ * @var array
+ */
+ public static $modules = array('contact', 'config_translation', 'config_translation_test', 'views', 'views_ui', 'contextual');
+
+ /**
+ * Languages to enable.
+ *
+ * @var array
+ */
+ protected $langcodes = array('fr', 'ta');
+
+ /**
+ * Administrator user for tests.
+ *
+ * @var \Drupal\user\UserInterface
+ */
+ protected $admin_user;
+
+ /**
+ * Translator user for tests.
+ *
+ * @var \Drupal\user\UserInterface
+ */
+ protected $translator_user;
+
+ /**
+ * String translation storage object.
+ *
+ * @var \Drupal\locale\StringStorageInterface
+ */
+ protected $localeStorage;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Configuration Translation',
+ 'description' => 'Translate settings and entities to various languages',
+ 'group' => 'Configuration Translation',
+ );
+ }
+
+ public function setUp() {
+ parent::setUp();
+ $translator_permissions = array(
+ 'translate configuration',
+ );
+ $admin_permissions = array_merge(
+ $translator_permissions,
+ array(
+ 'administer languages',
+ 'administer site configuration',
+ 'administer contact forms',
+ 'access site-wide contact form',
+ 'access contextual links',
+ 'administer views',
+ )
+ );
+ // Create and login user.
+ $this->translator_user = $this->drupalCreateUser($translator_permissions);
+ $this->admin_user = $this->drupalCreateUser($admin_permissions);
+
+ // Add languages.
+ foreach ($this->langcodes as $langcode) {
+ $language = new Language(array('id' => $langcode));
+ language_save($language);
+ }
+ $this->localeStorage = $this->container->get('locale.storage');
+ }
+
+ /**
+ * Tests the site information translation interface.
+ */
+ public function testSiteInformationTranslationUi() {
+ $this->drupalLogin($this->admin_user);
+
+ $site_name = 'Site name for testing configuration translation';
+ $site_slogan = 'Site slogan for testing configuration translation';
+ $fr_site_name = 'Nom du site pour tester la configuration traduction';
+ $fr_site_slogan = 'Slogan du site pour tester la traduction de configuration';
+ $translation_base_url = 'admin/config/system/site-information/translate';
+
+ // Set site name and slogan for default language.
+ $this->setSiteInformation($site_name, $site_slogan);
+
+ $this->drupalGet('admin/config/system/site-information');
+ // Check translation tab exist.
+ $this->assertLinkByHref($translation_base_url);
+
+ $this->drupalGet($translation_base_url);
+
+ // Check that the 'Edit' link in the source language links back to the
+ // original form.
+ $this->clickLink(t('Edit'));
+ // Also check that saving the form leads back to the translation overview.
+ $this->drupalPostForm(NULL, array(), t('Save configuration'));
+ $this->assertUrl($translation_base_url);
+
+ // Check 'Add' link of French to visit add page.
+ $this->assertLinkByHref("$translation_base_url/fr/add");
+ $this->clickLink(t('Add'));
+
+ // Make sure original text is present on this page.
+ $this->assertRaw($site_name);
+ $this->assertRaw($site_slogan);
+
+ // Update site name and slogan for French.
+ $edit = array(
+ 'config_names[system.site][name][translation]' => $fr_site_name,
+ 'config_names[system.site][slogan][translation]' => $fr_site_slogan,
+ );
+
+ $this->drupalPostForm("$translation_base_url/fr/add", $edit, t('Save translation'));
+ $this->assertRaw(t('Successfully saved @language translation.', array('@language' => 'French')));
+
+ // Check for edit, delete links (and no 'add' link) for French language.
+ $this->assertNoLinkByHref("$translation_base_url/fr/add");
+ $this->assertLinkByHref("$translation_base_url/fr/edit");
+ $this->assertLinkByHref("$translation_base_url/fr/delete");
+
+ // Check translation saved proper.
+ $this->drupalGet("$translation_base_url/fr/edit");
+ $this->assertFieldByName('config_names[system.site][name][translation]', $fr_site_name);
+ $this->assertFieldByName('config_names[system.site][slogan][translation]', $fr_site_slogan);
+
+ // Check French translation of site name and slogan are in place.
+ $this->drupalGet('fr');
+ $this->assertRaw($fr_site_name);
+ $this->assertRaw($fr_site_slogan);
+
+ // Visit French site to ensure base language string present as source.
+ $this->drupalGet("fr/$translation_base_url/fr/edit");
+ $this->assertText($site_name);
+ $this->assertText($site_slogan);
+ }
+
+ /**
+ * Tests the site information translation interface.
+ */
+ public function testSourceValueDuplicateSave() {
+ $this->drupalLogin($this->admin_user);
+
+ $site_name = 'Site name for testing configuration translation';
+ $site_slogan = 'Site slogan for testing configuration translation';
+ $translation_base_url = 'admin/config/system/site-information/translate';
+ $this->setSiteInformation($site_name, $site_slogan);
+
+ $this->drupalGet($translation_base_url);
+
+ // Case 1: Update new value for site slogan and site name.
+ $edit = array(
+ 'config_names[system.site][name][translation]' => 'FR ' . $site_name,
+ 'config_names[system.site][slogan][translation]' => 'FR ' . $site_slogan,
+ );
+ // First time, no overrides, so just Add link.
+ $this->drupalPostForm("$translation_base_url/fr/add", $edit, t('Save translation'));
+
+ // Read overridden file from active config.
+ $file_storage = new FileStorage($this->configDirectories[CONFIG_ACTIVE_DIRECTORY]);
+ $config_parsed = $file_storage->read('locale.config.fr.system.site');
+
+ // Expect both name and slogan in language specific file.
+ $expected = array(
+ 'name' => 'FR ' . $site_name,
+ 'slogan' => 'FR ' . $site_slogan,
+ );
+ $this->assertEqual($expected, $config_parsed);
+
+ // Case 2: Update new value for site slogan and default value for site name.
+ $this->drupalGet("$translation_base_url/fr/edit");
+ // Assert that the language configuration does not leak outside of the
+ // translation form into the actual site name and slogan.
+ $this->assertNoText('FR ' . $site_name);
+ $this->assertNoText('FR ' . $site_slogan);
+ $edit = array(
+ 'config_names[system.site][name][translation]' => $site_name,
+ 'config_names[system.site][slogan][translation]' => 'FR ' . $site_slogan,
+ );
+ $this->drupalPostForm(NULL, $edit, t('Save translation'));
+ $this->assertRaw(t('Successfully updated @language translation.', array('@language' => 'French')));
+ $config_parsed = $file_storage->read('locale.config.fr.system.site');
+
+ // Expect only slogan in language specific file.
+ $expected = array('slogan' => 'FR ' . $site_slogan);
+ $this->assertEqual($expected, $config_parsed);
+
+ // Case 3: Keep default value for site name and slogan.
+ $this->drupalGet("$translation_base_url/fr/edit");
+ $this->assertNoText('FR ' . $site_slogan);
+ $edit = array(
+ 'config_names[system.site][name][translation]' => $site_name,
+ 'config_names[system.site][slogan][translation]' => $site_slogan,
+ );
+ $this->drupalPostForm(NULL, $edit, t('Save translation'));
+ $config_parsed = $file_storage->read('locale.config.fr.system.site');
+
+ // Expect no language specific file.
+ $this->assertFalse($config_parsed);
+
+ // Check configuration page with translator user. Should have no access.
+ $this->drupalLogout();
+ $this->drupalLogin($this->translator_user);
+ $this->drupalGet('admin/config/system/site-information');
+ $this->assertResponse(403);
+
+ // While translator can access the translation page, the edit link is not
+ // present due to lack of permissions.
+ $this->drupalGet($translation_base_url);
+ $this->assertNoLink(t('Edit'));
+
+ // Check 'Add' link for French.
+ $this->assertLinkByHref("$translation_base_url/fr/add");
+ }
+
+ /**
+ * Tests the contact category translation.
+ */
+ public function testContactConfigEntityTranslation() {
+ $this->drupalLogin($this->admin_user);
+
+ $file_storage = new FileStorage($this->configDirectories[CONFIG_ACTIVE_DIRECTORY]);
+
+ $this->drupalGet('admin/structure/contact');
+
+ // Check for default contact form configuration entity from Contact module.
+ $this->assertLinkByHref('admin/structure/contact/manage/feedback');
+
+ // Save default language configuration.
+ $label = 'Send your feedback';
+ $edit = array(
+ 'label' => $label,
+ 'recipients' => 'sales@example.com,support@example.com',
+ 'reply' => 'Thank you for your mail',
+ );
+ $this->drupalPostForm('admin/structure/contact/manage/feedback', $edit, t('Save'));
+
+ // Ensure translation link is present.
+ $translation_base_url = 'admin/structure/contact/manage/feedback/translate';
+ $this->assertLinkByHref($translation_base_url);
+
+ // Visit the form to confirm the changes.
+ $this->drupalGet('contact/feedback');
+ $this->assertText($label);
+
+ foreach ($this->langcodes as $langcode) {
+ $this->drupalGet($translation_base_url);
+
+ // 'Add' link should be present for $langcode translation.
+ $translation_page_url = "$translation_base_url/$langcode/add";
+ $this->assertLinkByHref($translation_page_url);
+
+ // Make sure original text is present on this page.
+ $this->drupalGet($translation_page_url);
+ $this->assertText($label);
+
+ // Update translatable fields.
+ $edit = array(
+ 'config_names[contact.category.feedback][label][translation]' => 'Website feedback - ' . $langcode,
+ 'config_names[contact.category.feedback][reply][translation]' => 'Thank you for your mail - ' . $langcode,
+ );
+
+ // Save language specific version of form.
+ $this->drupalPostForm($translation_page_url, $edit, t('Save translation'));
+
+ // Expect translated values in language specific file.
+ $config_parsed = $file_storage->read('locale.config.'. $langcode . '.contact.category.feedback');
+ $expected = array(
+ 'label' => 'Website feedback - ' . $langcode,
+ 'reply' => 'Thank you for your mail - ' . $langcode,
+ );
+ $this->assertEqual($expected, $config_parsed);
+
+ // Check for edit, delete links (and no 'add' link) for $langcode.
+ $this->assertNoLinkByHref("$translation_base_url/$langcode/add");
+ $this->assertLinkByHref("$translation_base_url/$langcode/edit");
+ $this->assertLinkByHref("$translation_base_url/$langcode/delete");
+
+ // Visit language specific version of form to check label.
+ $this->drupalGet($langcode . '/contact/feedback');
+ $this->assertText('Website feedback - ' . $langcode);
+
+ // Submit feedback.
+ $edit = array(
+ 'subject' => 'Test subject',
+ 'message' => 'Test message',
+ );
+ $this->drupalPostForm(NULL, $edit, t('Send message'));
+ }
+
+ // Now that all language translations are present, check translation and
+ // original text all appear in any translated page on the translation
+ // forms.
+ foreach ($this->langcodes as $langcode) {
+ $langcode_prefixes = array_merge(array(''), $this->langcodes);
+ foreach ($langcode_prefixes as $langcode_prefix) {
+ $this->drupalGet(ltrim("$langcode_prefix/$translation_base_url/$langcode/edit"));
+ $this->assertFieldByName('config_names[contact.category.feedback][label][translation]', 'Website feedback - ' . $langcode);
+ $this->assertText($label);
+ }
+ }
+
+ // We get all emails so no need to check inside the loop.
+ $captured_emails = $this->drupalGetMails();
+
+ // Check language specific auto reply text in email body.
+ foreach ($captured_emails as $email) {
+ if ($email['id'] == 'contact_page_autoreply') {
+ // Trim because we get an added newline for the body.
+ $this->assertEqual(trim($email['body']), 'Thank you for your mail - ' . $email['langcode']);
+ }
+ }
+
+ // Test that delete links work and operations perform properly.
+ foreach ($this->langcodes as $langcode) {
+ $replacements = array('%label' => t('!label !entity_type', array('!label' => $label, '!entity_type' => Unicode::strtolower(t('Contact category')))), '@language' => language_load($langcode)->name);
+
+ $this->drupalGet("$translation_base_url/$langcode/delete");
+ $this->assertRaw(t('Are you sure you want to delete the @language translation of %label?', $replacements));
+ // Assert link back to list page to cancel delete is present.
+ $this->assertLinkByHref($translation_base_url);
+
+ $this->drupalPostForm(NULL, array(), t('Delete'));
+ $this->assertRaw(t('@language translation of %label was deleted', $replacements));
+ $this->assertLinkByHref("$translation_base_url/$langcode/add");
+ $this->assertNoLinkByHref("translation_base_url/$langcode/edit");
+ $this->assertNoLinkByHref("$translation_base_url/$langcode/delete");
+
+ // Expect no language specific file present anymore.
+ $config_parsed = $file_storage->read('locale.config.'. $langcode . '.config.category.feedback');
+ $this->assertFalse($config_parsed);
+ }
+
+ // Check configuration page with translator user. Should have no access.
+ $this->drupalLogout();
+ $this->drupalLogin($this->translator_user);
+ $this->drupalGet('admin/structure/contact/manage/feedback');
+ $this->assertResponse(403);
+
+ // While translator can access the translation page, the edit link is not
+ // present due to lack of permissions.
+ $this->drupalGet($translation_base_url);
+ $this->assertNoLink(t('Edit'));
+
+ // Check 'Add' link for French.
+ $this->assertLinkByHref("$translation_base_url/fr/add");
+ }
+
+ /**
+ * Tests date format translation.
+ */
+ public function testDateFormatTranslation() {
+ $this->drupalLogin($this->admin_user);
+ $file_storage = new FileStorage($this->configDirectories[CONFIG_ACTIVE_DIRECTORY]);
+
+ $this->drupalGet('admin/config/regional/date-time');
+
+ // Check for medium format.
+ $this->assertLinkByHref('admin/config/regional/date-time/formats/manage/medium');
+
+ // Save default language configuration for a new format.
+ $edit = array(
+ 'label' => 'Custom medium date',
+ 'id' => 'custom_medium',
+ 'date_format_pattern' => 'Y. m. d. H:i',
+ );
+ $this->drupalPostForm('admin/config/regional/date-time/formats/add', $edit, t('Add format'));
+
+ // Test translating a default shipped format and our custom format.
+ $formats = array(
+ 'medium' => 'Default medium date',
+ 'custom_medium' => 'Custom medium date',
+ );
+ foreach($formats as $id => $label) {
+ $translation_base_url = 'admin/config/regional/date-time/formats/manage/' . $id . '/translate';
+
+ $this->drupalGet($translation_base_url);
+
+ // 'Add' link should be present for French translation.
+ $translation_page_url = "$translation_base_url/fr/add";
+ $this->assertLinkByHref($translation_page_url);
+
+ // Make sure original text is present on this page.
+ $this->drupalGet($translation_page_url);
+ $this->assertText($label);
+
+ // Update translatable fields.
+ $edit = array(
+ 'config_names[system.date_format.' . $id . '][label][translation]' => $id . ' - FR',
+ 'config_names[system.date_format.' . $id . '][pattern][pattern.php][translation]' => 'D',
+ );
+
+ // Save language specific version of form.
+ $this->drupalPostForm($translation_page_url, $edit, t('Save translation'));
+
+ // Get translation and check we've got the right value.
+ $config_parsed = $file_storage->read('locale.config.fr.system.date_format.' . $id);
+ $expected = array(
+ 'label' => $id . ' - FR',
+ 'pattern' => array('php' => 'D'),
+ );
+ $this->assertEqual($expected, $config_parsed);
+
+ // Formatting the date 8 / 27 / 1985 @ 13:37 EST with pattern D should
+ // display "Tue".
+ $formatted_date = format_date(494015820, $id, NULL, NULL, 'fr');
+ $this->assertEqual($formatted_date, 'Tue', 'Got the right formatted date using the date format translation pattern.');
+ }
+ }
+
+ /**
+ * Tests the account settings translation interface.
+ *
+ * This is the only special case so far where we have multiple configuration
+ * names involved building up one configuration translation form. Test that
+ * the translations are saved for all configuration names properly.
+ */
+ public function testAccountSettingsConfigurationTranslation() {
+ $this->drupalLogin($this->admin_user);
+
+ $this->drupalGet('admin/config/people/accounts/translate');
+ $this->assertLinkByHref('admin/config/people/accounts/translate/fr/add');
+
+ // Update account settings fields for French.
+ $edit = array(
+ 'config_names[user.settings][anonymous][translation]' => 'Anonyme',
+ 'config_names[user.mail][status_blocked][status_blocked.subject][translation]' => 'Testing, your account is blocked.',
+ 'config_names[user.mail][status_blocked][status_blocked.body][translation]' => 'Testing account blocked body.',
+ );
+
+ $this->drupalPostForm('admin/config/people/accounts/translate/fr/add', $edit, t('Save translation'));
+
+ // Make sure the changes are saved and loaded back properly.
+ $this->drupalGet('admin/config/people/accounts/translate/fr/edit');
+ foreach ($edit as $key => $value) {
+ // Check the translations appear in the right field type as well.
+ $xpath = '//' . (strpos($key, '.body') ? 'textarea' : 'input') . '[@name="'. $key . '"]';
+ $this->assertFieldByXPath($xpath, $value);
+ }
+ // Check that labels for email settings appear.
+ $this->assertText(t('Account cancellation confirmation'));
+ $this->assertText(t('Password recovery'));
+ }
+
+ /**
+ * Tests source and target language edge cases.
+ */
+ public function testSourceAndTargetLanguage() {
+ $this->drupalLogin($this->admin_user);
+
+ // Loading translation page for not-specified language (und)
+ // should return 403.
+ $this->drupalGet('admin/config/system/site-information/translate/und/add');
+ $this->assertResponse(403);
+
+ // Check the source language doesn't have 'Add' or 'Delete' link and
+ // make sure source language edit goes to original configuration page
+ // not the translation specific edit page.
+ $this->drupalGet('admin/config/system/site-information/translate');
+ $this->assertNoLinkByHref('admin/config/system/site-information/translate/en/edit');
+ $this->assertNoLinkByHref('admin/config/system/site-information/translate/en/add');
+ $this->assertNoLinkByHref('admin/config/system/site-information/translate/en/delete');
+ $this->assertLinkByHref('admin/config/system/site-information');
+
+ // Translation addition to source language should return 403.
+ $this->drupalGet('admin/config/system/site-information/translate/en/add');
+ $this->assertResponse(403);
+
+ // Translation editing in source language should return 403.
+ $this->drupalGet('admin/config/system/site-information/translate/en/edit');
+ $this->assertResponse(403);
+
+ // Translation deletion in source language should return 403.
+ $this->drupalGet('admin/config/system/site-information/translate/en/delete');
+ $this->assertResponse(403);
+
+ // Set default language of site information to not-specified language (und).
+ $this->container
+ ->get('config.factory')
+ ->get('system.site')
+ ->set('langcode', Language::LANGCODE_NOT_SPECIFIED)
+ ->save();
+
+ // Make sure translation tab does not exist on the configuration page.
+ $this->drupalGet('admin/config/system/site-information');
+ $this->assertNoLinkByHref('admin/config/system/site-information/translate');
+
+ // If source language is not specified, translation page should be 403.
+ $this->drupalGet('admin/config/system/site-information/translate');
+ $this->assertResponse(403);
+ }
+
+ /**
+ * Tests the views translation interface.
+ */
+ public function testViewsTranslationUI() {
+ $this->drupalLogin($this->admin_user);
+
+ // Assert contextual link related to views.
+ $ids = array('views_ui_edit:view=frontpage:location=page&name=frontpage&display_id=page_1');
+ $response = $this->renderContextualLinks($ids, 'node');
+ $this->assertResponse(200);
+ $json = Json::decode($response);
+ $this->assertTrue(strpos($json[$ids[0]], t('Translate view')), 'Translate view contextual link added.');
+
+ $description = 'All content promoted to the front page.';
+ $human_readable_name = 'Frontpage';
+ $display_settings_master = 'Master';
+ $display_options_master = '(Empty)';
+ $translation_base_url = 'admin/structure/views/view/frontpage/translate';
+
+ $this->drupalGet($translation_base_url);
+
+ // Check 'Add' link of French to visit add page.
+ $this->assertLinkByHref("$translation_base_url/fr/add");
+ $this->clickLink(t('Add'));
+
+ // Make sure original text is present on this page.
+ $this->assertRaw($description);
+ $this->assertRaw($human_readable_name);
+
+ // Update Views Fields for French.
+ $edit = array(
+ 'config_names[views.view.frontpage][description][translation]' => $description . " FR",
+ 'config_names[views.view.frontpage][label][translation]' => $human_readable_name . " FR",
+ 'config_names[views.view.frontpage][display][default][display.default.display_title][translation]' => $display_settings_master . " FR",
+ 'config_names[views.view.frontpage][display][default][display_options][display.default.display_options.title][translation]' => $display_options_master . " FR",
+ );
+ $this->drupalPostForm("$translation_base_url/fr/add", $edit, t('Save translation'));
+ $this->assertRaw(t('Successfully saved @language translation.', array('@language' => 'French')));
+
+ // Check for edit, delete links (and no 'add' link) for French language.
+ $this->assertNoLinkByHref("$translation_base_url/fr/add");
+ $this->assertLinkByHref("$translation_base_url/fr/edit");
+ $this->assertLinkByHref("$translation_base_url/fr/delete");
+
+ // Check translation saved proper.
+ $this->drupalGet("$translation_base_url/fr/edit");
+ $this->assertFieldByName('config_names[views.view.frontpage][description][translation]', $description . " FR");
+ $this->assertFieldByName('config_names[views.view.frontpage][label][translation]', $human_readable_name . " FR");
+ $this->assertFieldByName('config_names[views.view.frontpage][display][default][display.default.display_title][translation]', $display_settings_master . " FR");
+ $this->assertFieldByName('config_names[views.view.frontpage][display][default][display_options][display.default.display_options.title][translation]', $display_options_master . " FR");
+ }
+
+ /**
+ * Test translation storage in locale storage.
+ */
+ public function testLocaleDBStorage() {
+ $this->drupalLogin($this->admin_user);
+
+ $langcode = 'xx';
+ $name = $this->randomName(16);
+ $edit = array(
+ 'predefined_langcode' => 'custom',
+ 'langcode' => $langcode,
+ 'name' => $name,
+ 'direction' => '0',
+ );
+ $this->drupalPostForm('admin/config/regional/language/add', $edit, t('Add custom language'));
+
+ // Make sure there is no translation stored in locale storage before edit.
+ $translation = $this->getTranslation('user.settings', 'anonymous', 'fr');
+ $this->assertTrue(empty($translation));
+
+ // Add custom translation.
+ $edit = array(
+ 'config_names[user.settings][anonymous][translation]' => 'Anonyme',
+ );
+ $this->drupalPostForm('admin/config/people/accounts/translate/fr/add', $edit, t('Save translation'));
+
+ // Make sure translation stored in locale storage after saved language
+ // specific configuration translation.
+ $translation = $this->getTranslation('user.settings', 'anonymous', 'fr');
+ $this->assertEqual('Anonyme', $translation->getString());
+
+ // revert custom translations to base translation.
+ $edit = array(
+ 'config_names[user.settings][anonymous][translation]' => 'Anonymous',
+ );
+ $this->drupalPostForm('admin/config/people/accounts/translate/fr/edit', $edit, t('Save translation'));
+
+ // Make sure there is no translation stored in locale storage after revert.
+ $translation = $this->getTranslation('user.settings', 'anonymous', 'fr');
+ $this->assertEqual('Anonymous', $translation->getString());
+ }
+
+ /**
+ * Tests the single language existing.
+ */
+ public function testSingleLanguageUI() {
+ $this->drupalLogin($this->admin_user);
+
+ // Delete French language
+ $this->drupalPostForm('admin/config/regional/language/delete/fr', array(), t('Delete'));
+ $this->assertRaw(t('The %language (%langcode) language has been removed.', array('%language' => 'French', '%langcode' => 'fr')));
+
+ // Change default language to Tamil.
+ $edit = array(
+ 'site_default_language' => 'ta',
+ );
+ $this->drupalPostForm('admin/config/regional/settings', $edit, t('Save configuration'));
+ $this->assertRaw(t('The configuration options have been saved.'));
+
+ // Delete English language
+ $this->drupalPostForm('admin/config/regional/language/delete/en', array(), t('Delete'));
+ $this->assertRaw(t('The %language (%langcode) language has been removed.', array('%language' => 'English', '%langcode' => 'en')));
+
+ // Visit account setting translation page, this should not
+ // throw any notices.
+ $this->drupalGet('admin/config/people/accounts/translate');
+ $this->assertResponse(200);
+ }
+
+ /**
+ * Tests the config_translation_info_alter() hook.
+ */
+ public function testAlterInfo() {
+ $this->drupalLogin($this->admin_user);
+
+ $this->container->get('state')->set('config_translation_test_config_translation_info_alter', TRUE);
+ $this->container->get('plugin.manager.config_translation.mapper')->clearCachedDefinitions();
+
+ // Check out if the translation page has the altered in settings.
+ $this->drupalGet('admin/config/system/site-information/translate/fr/add');
+ $this->assertText(t('Feed channel'));
+ $this->assertText(t('Feed description'));
+
+ // Check if the translation page does not have the altered out settings.
+ $this->drupalGet('admin/config/people/accounts/translate/fr/add');
+ $this->assertText(t('Name'));
+ $this->assertNoText(t('Account cancellation confirmation'));
+ $this->assertNoText(t('Password recovery'));
+ }
+
+ /**
+ * Tests that theme provided *.config_translation.yml files are found.
+ */
+ public function testThemeDiscovery() {
+ // Enable the test theme and rebuild routes.
+ $theme = 'config_translation_test_theme';
+ theme_enable(array($theme));
+ \Drupal::service('router.builder')->rebuild();
+ menu_router_rebuild();
+
+ $this->drupalLogin($this->admin_user);
+
+ $translation_base_url = 'admin/config/development/performance/translate';
+ $this->drupalGet($translation_base_url);
+ $this->assertResponse(200);
+ $this->assertLinkByHref("$translation_base_url/fr/add");
+ }
+
+ /**
+ * Gets translation from locale storage.
+ *
+ * @param $config_name
+ * Configuration object.
+ * @param $key
+ * Translation configuration field key.
+ * @param $langcode
+ * String language code to load translation.
+ *
+ * @return bool|mixed
+ * Returns translation if exists, FALSE otherwise.
+ */
+ protected function getTranslation($config_name, $key, $langcode) {
+ $settings_locations = $this->localeStorage->getLocations(array('type' => 'configuration', 'name' => $config_name));
+ $this->assertTrue(!empty($settings_locations), format_string('Configuration locations found for %config_name.', array('%config_name' => $config_name)));
+
+ if (!empty($settings_locations)) {
+ $source = $this->container->get('config.factory')->get($config_name)->get($key);
+ $source_string = $this->localeStorage->findString(array('source' => $source, 'type' => 'configuration'));
+ $this->assertTrue(!empty($source_string), format_string('Found string for %config_name.%key.', array('%config_name' => $config_name, '%key' => $key)));
+
+ if (!empty($source_string)) {
+ $conditions = array(
+ 'lid' => $source_string->lid,
+ 'language' => $langcode,
+ );
+ $translations = $this->localeStorage->getTranslations($conditions + array('translated' => TRUE));
+ return reset($translations);
+ }
+ }
+ return FALSE;
+ }
+
+ /**
+ * Sets site name and slogan for default language, helps in tests.
+ *
+ * @param string $site_name
+ * @param string $site_slogan
+ */
+ protected function setSiteInformation($site_name, $site_slogan) {
+ $edit = array(
+ 'site_name' => $site_name,
+ 'site_slogan' => $site_slogan,
+ );
+ $this->drupalPostForm('admin/config/system/site-information', $edit, t('Save configuration'));
+ $this->assertRaw(t('The configuration options have been saved.'));
+ }
+
+ /**
+ * Get server-rendered contextual links for the given contextual link ids.
+ *
+ * @param array $ids
+ * An array of contextual link ids.
+ * @param string $current_path
+ * The Drupal path for the page for which the contextual links are rendered.
+ *
+ * @return string
+ * The response body.
+ */
+ protected function renderContextualLinks($ids, $current_path) {
+ $post = array();
+ for ($i = 0; $i < count($ids); $i++) {
+ $post['ids[' . $i . ']'] = $ids[$i];
+ }
+ return $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => $current_path)));
+ }
+
+}
diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationViewListUiTest.php b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationViewListUiTest.php
new file mode 100644
index 0000000..a0b65e9
--- /dev/null
+++ b/core/modules/config_translation/lib/Drupal/config_translation/Tests/ConfigTranslationViewListUiTest.php
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Tests\ConfigTranslationViewListUiTest.
+ */
+
+namespace Drupal\config_translation\Tests;
+
+use Drupal\views_ui\Tests\UITestBase;
+
+/**
+ * Tests for the views_ui module that should have translate operation.
+ */
+class ConfigTranslationViewListUiTest extends UITestBase {
+
+ /**
+ * Views used by this test.
+ *
+ * @var array
+ */
+ public static $testViews = array('test_view');
+
+ /**
+ * Modules to enable.
+ *
+ * @var array
+ */
+ public static $modules = array('config_translation', 'views_ui');
+
+ public function setUp() {
+ parent::setUp();
+
+ $permissions = array(
+ 'administer views',
+ 'translate configuration',
+ );
+
+ // Create and log in user.
+ $this->drupalLogin($this->drupalCreateUser($permissions));
+ }
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Configuration Translation view list',
+ 'description' => 'Visit view list and test if translate is available.',
+ 'group' => 'Configuration Translation',
+ );
+ }
+
+ /**
+ * Tests views_ui list to see if translate link is added to operations.
+ */
+ public function testTranslateOperationInViewListUi() {
+ // Views UI List 'admin/structure/views'.
+ $this->drupalGet('admin/structure/views');
+ $translate_link = 'admin/structure/views/view/test_view/translate';
+ // Test if the link to translate the test_view is on the page.
+ $this->assertLinkByHref($translate_link);
+
+ // Test if the link to translate actually goes to the translate page.
+ $this->drupalGet($translate_link);
+ $this->assertRaw('<th>' . t('Language') . '</th>');
+ }
+
+}
diff --git a/core/modules/config_translation/templates/config_translation_manage_form_element.html.twig b/core/modules/config_translation/templates/config_translation_manage_form_element.html.twig
new file mode 100644
index 0000000..6bbee3d
--- /dev/null
+++ b/core/modules/config_translation/templates/config_translation_manage_form_element.html.twig
@@ -0,0 +1,24 @@
+{#
+/**
+* @file
+* Default theme implementation for a form element in config_translation.
+*
+* Available variables:
+* - element: Array that represents the element shown in the form.
+* - source: The source of the translation.
+* - translation: The translation for the target language.
+*
+* @see template_preprocess()
+* @see template_preprocess_config_translation_manage_form_element()
+*
+* @ingroup themeable
+*/
+#}
+<div class="clearfix translation-element-wrapper">
+ <div class="source">
+ {{ element.source }}
+ </div>
+ <div class="translation">
+ {{ element.translation }}
+ </div>
+</div>
diff --git a/core/modules/config_translation/tests/Drupal/config_translation/Tests/ConfigEntityMapperTest.php b/core/modules/config_translation/tests/Drupal/config_translation/Tests/ConfigEntityMapperTest.php
new file mode 100644
index 0000000..df9aa0c
--- /dev/null
+++ b/core/modules/config_translation/tests/Drupal/config_translation/Tests/ConfigEntityMapperTest.php
@@ -0,0 +1,193 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Tests\ConfigEntityMapperTest.
+ */
+
+namespace Drupal\config_translation\Tests;
+
+use Drupal\config_translation\ConfigEntityMapper;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Tests ConfigEntityMapper.
+ *
+ * @group Drupal
+ * @group Config_translation
+ */
+class ConfigEntityMapperTest extends UnitTestCase {
+
+ /**
+ * The configuration entity mapper to test.
+ *
+ * @var \Drupal\config_translation\ConfigEntityMapper
+ */
+ protected $configEntityMapper;
+
+ /**
+ * The entity manager used for testing.
+ *
+ * @var \Drupal\Core\Config\TypedConfigManager|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $entityManager;
+
+ /**
+ * The entity instance used for testing.
+ *
+ * @var \Drupal\Core\Config\TypedConfigManager|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $entity;
+
+ /**
+ * The route provider used for testing.
+ *
+ * @var \Drupal\Core\Routing\RouteProviderInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $routeProvider;
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getInfo() {
+ return array(
+ 'name' => 'Configuration entity mapper',
+ 'description' => 'Tests the functionality provided by the configuration entity mapper.',
+ 'group' => 'Configuration Translation',
+ );
+ }
+
+ public function setUp() {
+ $this->entityManager = $this->getMockBuilder('Drupal\Core\Entity\EntityManager')
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->entity = $this->getMock('Drupal\Core\Entity\EntityInterface');
+
+ $this->routeProvider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface');
+
+ $this->routeProvider
+ ->expects($this->once())
+ ->method('getRouteByName')
+ ->with('language.edit')
+ ->will($this->returnValue(new Route('/admin/config/regional/language/edit/{language_entity}')));
+
+ $definition = array(
+ 'class' => '\Drupal\config_translation\ConfigEntityMapper',
+ 'base_route_name' => 'language.edit',
+ 'title' => '!label language',
+ 'names' => array(),
+ 'entity_type' => 'language_entity',
+ 'route_name' => 'config_translation.item.overview.language.edit',
+ );
+ $locale_config_manager = $this->getMockBuilder('Drupal\locale\LocaleConfigManager')
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configEntityMapper = new ConfigEntityMapper(
+ 'language_entity',
+ $definition,
+ $this->getConfigFactoryStub(),
+ $locale_config_manager,
+ $this->getMock('Drupal\config_translation\ConfigMapperManagerInterface'),
+ $this->routeProvider,
+ $this->getStringTranslationStub(),
+ $this->entityManager
+ );
+ }
+
+ /**
+ * Tests ConfigEntityMapper::setEntity().
+ */
+ public function testSetEntity() {
+ $this->entity
+ ->expects($this->once())
+ ->method('id')
+ ->with()
+ ->will($this->returnValue('entity_id'));
+
+ $this->entityManager
+ ->expects($this->once())
+ ->method('getDefinition')
+ ->with('language_entity')
+ ->will($this->returnValue(array('config_prefix' => 'config_prefix')));
+
+ $result = $this->configEntityMapper->setEntity($this->entity);
+ $this->assertTrue($result);
+
+ // Make sure setEntity() returns FALSE when called a second time.
+ $result = $this->configEntityMapper->setEntity($this->entity);
+ $this->assertFalse($result);
+ }
+
+ /**
+ * Tests ConfigEntityMapper::getOverviewRouteParameters().
+ */
+ public function testGetOverviewRouteParameters() {
+ $this->configEntityMapper->setEntity($this->entity);
+
+ $this->entity
+ ->expects($this->once())
+ ->method('id')
+ ->with()
+ ->will($this->returnValue('entity_id'));
+
+ $result = $this->configEntityMapper->getOverviewRouteParameters();
+
+ $this->assertSame(array('language_entity' => 'entity_id'), $result);
+ }
+
+ /**
+ * Tests ConfigEntityMapper::getType().
+ */
+ public function testGetType() {
+ $result = $this->configEntityMapper->getType();
+ $this->assertSame('language_entity', $result);
+ }
+
+ /**
+ * Tests ConfigEntityMapper::getTypeName().
+ */
+ public function testGetTypeName() {
+ $this->entityManager
+ ->expects($this->once())
+ ->method('getDefinition')
+ ->with('language_entity')
+ ->will($this->returnValue(array('label' => 'test')));
+
+ $result = $this->configEntityMapper->getTypeName();
+ $this->assertSame('test', $result);
+ }
+
+ /**
+ * Tests ConfigEntityMapper::getTypeLabel().
+ */
+ public function testGetTypeLabel() {
+ $this->entityManager
+ ->expects($this->once())
+ ->method('getDefinition')
+ ->with('language_entity')
+ ->will($this->returnValue(array('label' => 'test')));
+
+ $result = $this->configEntityMapper->getTypeLabel();
+ $this->assertSame('test', $result);
+ }
+
+ /**
+ * Tests ConfigEntityMapper::getOperations().
+ */
+ public function testGetOperations() {
+ $result = $this->configEntityMapper->getOperations();
+
+ $expected = array(
+ 'list' => array(
+ 'title' => 'List',
+ 'href' => 'admin/config/regional/config-translation/language_entity',
+ )
+ );
+
+ $this->assertSame($expected, $result);
+ }
+
+}
diff --git a/core/modules/config_translation/tests/Drupal/config_translation/Tests/ConfigMapperManagerTest.php b/core/modules/config_translation/tests/Drupal/config_translation/Tests/ConfigMapperManagerTest.php
new file mode 100644
index 0000000..b07e542
--- /dev/null
+++ b/core/modules/config_translation/tests/Drupal/config_translation/Tests/ConfigMapperManagerTest.php
@@ -0,0 +1,213 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Tests\ConfigMapperManagerTest.
+ */
+
+namespace Drupal\config_translation\Tests {
+
+use Drupal\config_translation\ConfigMapperManager;
+use Drupal\Core\Language\Language;
+use Drupal\Core\TypedData\TypedDataInterface;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests ConfigMapperManager.
+ *
+ * @group Drupal
+ * @group Config_translation
+ */
+class ConfigMapperManagerTest extends UnitTestCase {
+
+ /**
+ * The configuration mapper manager to test.
+ *
+ * @var \Drupal\config_translation\ConfigMapperManager
+ */
+ protected $configMapperManager;
+
+ /**
+ * The typed configuration manager used for testing.
+ *
+ * @var \Drupal\Core\Config\TypedConfigManager|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $typedConfigManager;
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getInfo() {
+ return array(
+ 'name' => 'Configuration translation mapper manager',
+ 'description' => 'Tests the functionality provided by configuration translation mapper manager.',
+ 'group' => 'Configuration Translation',
+ );
+ }
+
+ public function setUp() {
+ $language = new Language(array('id' => 'en'));
+ $language_manager = $this->getMock('Drupal\Core\Language\LanguageManager');
+ $language_manager->expects($this->once())
+ ->method('getLanguage')
+ ->with(Language::TYPE_INTERFACE)
+ ->will($this->returnValue($language));
+
+ $this->typedConfigManager = $this->getMockBuilder('Drupal\Core\Config\TypedConfigManager')
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $module_handler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
+ $module_handler->expects($this->once())
+ ->method('getModuleList')
+ ->with()
+ ->will($this->returnValue(array()));
+
+ $this->configMapperManager = new TestConfigMapperManager(
+ $this->getMock('Drupal\Core\Cache\CacheBackendInterface'),
+ $language_manager,
+ $module_handler,
+ $this->typedConfigManager
+ );
+ }
+
+ /**
+ * Tests ConfigMapperManager::hasTranslatable().
+ *
+ * @param \Drupal\Core\TypedData\TypedDataInterface $element
+ * The schema element to test.
+ * @param bool $expected
+ * The expected return value of ConfigMapperManager::hasTranslatable().
+ *
+ * @dataProvider providerTestHasTranslatable
+ */
+ public function testHasTranslatable(TypedDataInterface $element, $expected) {
+ $this->typedConfigManager
+ ->expects($this->once())
+ ->method('get')
+ ->with('test')
+ ->will($this->returnValue($element));
+
+ $result = $this->configMapperManager->hasTranslatable('test');
+ $this->assertSame($expected, $result);
+ }
+
+ /**
+ * Provides data for ConfigMapperManager::testHasTranslatable()
+ *
+ * @return array
+ * An array of arrays, where each inner array contains the schema element
+ * to test as the first key and the expected result of
+ * ConfigMapperManager::hasTranslatable() as the second key.
+ */
+ public function providerTestHasTranslatable() {
+ return array(
+ array($this->getElement(array()), FALSE),
+ array($this->getElement(array('aaa' => 'bbb')), FALSE),
+ array($this->getElement(array('translatable' => FALSE)), FALSE),
+ array($this->getElement(array('translatable' => TRUE)), TRUE),
+ array($this->getNestedElement(array(
+ $this->getElement(array()),
+ )), FALSE),
+ array($this->getNestedElement(array(
+ $this->getElement(array('translatable' => TRUE)),
+ )), TRUE),
+ array($this->getNestedElement(array(
+ $this->getElement(array('aaa' => 'bbb')),
+ $this->getElement(array('ccc' => 'ddd')),
+ $this->getElement(array('eee' => 'fff')),
+ )), FALSE),
+ array($this->getNestedElement(array(
+ $this->getElement(array('aaa' => 'bbb')),
+ $this->getElement(array('ccc' => 'ddd')),
+ $this->getElement(array('translatable' => TRUE)),
+ )), TRUE),
+ array($this->getNestedElement(array(
+ $this->getElement(array('aaa' => 'bbb')),
+ $this->getNestedElement(array(
+ $this->getElement(array('ccc' => 'ddd')),
+ $this->getElement(array('eee' => 'fff')),
+ )),
+ $this->getNestedElement(array(
+ $this->getElement(array('ggg' => 'hhh')),
+ $this->getElement(array('iii' => 'jjj')),
+ )),
+ )), FALSE),
+ array($this->getNestedElement(array(
+ $this->getElement(array('aaa' => 'bbb')),
+ $this->getNestedElement(array(
+ $this->getElement(array('ccc' => 'ddd')),
+ $this->getElement(array('eee' => 'fff')),
+ )),
+ $this->getNestedElement(array(
+ $this->getElement(array('ggg' => 'hhh')),
+ $this->getElement(array('translatable' => TRUE)),
+ )),
+ )), TRUE),
+ );
+ }
+
+ /**
+ * Returns a mocked schema element.
+ *
+ * @param array $definition
+ * The definition of the schema element.
+ *
+ * @return \Drupal\Core\Config\Schema\Element
+ * The mocked schema element.
+ */
+ protected function getElement(array $definition) {
+ $element = $this->getMock('Drupal\Core\TypedData\TypedDataInterface');
+ $element->expects($this->any())
+ ->method('getDefinition')
+ ->will($this->returnValue($definition));
+ return $element;
+ }
+
+ /**
+ * Returns a mocked nested schema element.
+ *
+ * @param array $elements
+ * An array of simple schema elements.
+ *
+ * @return \Drupal\Core\Config\Schema\Mapping
+ * A nested schema element, containing the passed-in elements.
+ */
+ protected function getNestedElement(array $elements) {
+ // ConfigMapperManager::findTranslatable() checks for the abstract class
+ // \Drupal\Core\Config\Schema\ArrayElement, but mocking that directly does
+ // not work.
+ $nested_element = $this->getMockBuilder('Drupal\Core\Config\Schema\Mapping')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $nested_element->expects($this->once())
+ ->method('getIterator')
+ ->will($this->returnValue(new \ArrayIterator($elements)));
+ return $nested_element;
+ }
+
+}
+
+/**
+ * Subclass of the tested class to avoid global function calls.
+ *
+ * @todo Remove this once https://drupal.org/node/2109287 is fixed in core.
+ */
+class TestConfigMapperManager extends ConfigMapperManager {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getThemeList($refresh = FALSE) {
+ return array();
+ }
+}
+
+}
+
+// @todo Remove this once https://drupal.org/node/2109287 is fixed in core.
+namespace {
+ if (!function_exists('drupal_get_path')) {
+ function drupal_get_path() {}
+ }
+}
diff --git a/core/modules/config_translation/tests/Drupal/config_translation/Tests/ConfigNamesMapperTest.php b/core/modules/config_translation/tests/Drupal/config_translation/Tests/ConfigNamesMapperTest.php
new file mode 100644
index 0000000..3df6ac0
--- /dev/null
+++ b/core/modules/config_translation/tests/Drupal/config_translation/Tests/ConfigNamesMapperTest.php
@@ -0,0 +1,656 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\Tests\ConfigNamesMapperTest.
+ */
+
+namespace Drupal\config_translation\Tests;
+
+use Drupal\config_translation\ConfigNamesMapper;
+use Drupal\Core\Config\ConfigFactory;
+use Drupal\Core\Language\Language;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Tests ConfigNamesMapper.
+ *
+ * @group Drupal
+ * @group Config_translation
+ */
+class ConfigNamesMapperTest extends UnitTestCase {
+
+ /**
+ * The plugin definition of the test mapper.
+ *
+ * @var array
+ */
+ protected $pluginDefinition;
+
+ /**
+ * The configuration names mapper to test.
+ *
+ * @see \Drupal\config_translation\ConfigNamesMapper
+ *
+ * @var \Drupal\config_translation\Tests\TestConfigNamesMapper
+ */
+ protected $configNamesMapper;
+
+ /**
+ * The locale configuration manager.
+ *
+ * @var \Drupal\locale\LocaleConfigManager|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $localeConfigManager;
+
+ /**
+ * The configuration mapper manager.
+ *
+ * @var \Drupal\config_translation\ConfigMapperManagerInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $configMapperManager;
+
+ /**
+ * The base route used for testing.
+ *
+ * @var \Symfony\Component\Routing\Route
+ */
+ protected $baseRoute;
+
+ /**
+ * The route provider used for testing.
+ *
+ * @var \Drupal\Core\Routing\RouteProviderInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $routeProvider;
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getInfo() {
+ return array(
+ 'name' => 'Configuration names mapper',
+ 'description' => 'Tests the functionality provided by the configuration names mapper.',
+ 'group' => 'Configuration Translation',
+ );
+ }
+
+ public function setUp() {
+ $this->routeProvider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface');
+
+ $this->pluginDefinition = array(
+ 'class' => '\Drupal\config_translation\ConfigNamesMapper',
+ 'base_route_name' => 'system.site_information_settings',
+ 'title' => 'System information',
+ 'names' => array('system.site'),
+ 'weight' => 42,
+ );
+
+ $this->localeConfigManager = $this->getMockBuilder('Drupal\locale\LocaleConfigManager')
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configMapperManager = $this->getMock('Drupal\config_translation\ConfigMapperManagerInterface');
+
+ $this->baseRoute = new Route('/admin/config/system/site-information');
+
+ $this->routeProvider
+ ->expects($this->once())
+ ->method('getRouteByName')
+ ->with('system.site_information_settings')
+ ->will($this->returnValue($this->baseRoute));
+
+ $this->configNamesMapper = new TestConfigNamesMapper(
+ 'system.site_information_settings',
+ $this->pluginDefinition,
+ $this->getConfigFactoryStub(),
+ $this->localeConfigManager,
+ $this->configMapperManager,
+ $this->routeProvider,
+ $this->getStringTranslationStub()
+ );
+ }
+
+ /**
+ * Tests ConfigNamesMapper::getTitle().
+ */
+ public function testGetTitle() {
+ $result = $this->configNamesMapper->getTitle();
+ $this->assertSame($this->pluginDefinition['title'], $result);
+ }
+
+ /**
+ * Tests ConfigNamesMapper::getBaseRouteName().
+ */
+ public function testGetBaseRouteName() {
+ $result = $this->configNamesMapper->getBaseRouteName();
+ $this->assertSame($this->pluginDefinition['base_route_name'], $result);
+ }
+
+ /**
+ * Tests ConfigNamesMapper::getBaseRouteParameters().
+ */
+ public function testGetBaseRouteParameters() {
+ $result = $this->configNamesMapper->getBaseRouteParameters();
+ $this->assertSame(array(), $result);
+ }
+
+ /**
+ * Tests ConfigNamesMapper::getBaseRoute().
+ */
+ public function testGetBaseRoute() {
+ $result = $this->configNamesMapper->getBaseRoute();
+ $this->assertSame($this->baseRoute, $result);
+ }
+
+ /**
+ * Tests ConfigNamesMapper::getBasePath().
+ */
+ public function testGetBasePath() {
+ $result = $this->configNamesMapper->getBasePath();
+ $this->assertSame('/admin/config/system/site-information', $result);
+ }
+
+ /**
+ * Tests ConfigNamesMapper::getOverviewRouteName().
+ */
+ public function testGetOverviewRouteName() {
+ $result = $this->configNamesMapper->getOverviewRouteName();
+ $expected = 'config_translation.item.overview.' . $this->pluginDefinition['base_route_name'];
+ $this->assertSame($expected, $result);
+ }
+
+ /**
+ * Tests ConfigNamesMapper::getOverviewRouteParameters().
+ */
+ public function testGetOverviewRouteParameters() {
+ $result = $this->configNamesMapper->getOverviewRouteParameters();
+ $this->assertSame(array(), $result);
+ }
+
+ /**
+ * Tests ConfigNamesMapper::getOverviewRoute().
+ */
+ public function testGetOverviewRoute() {
+ $expected = new Route('/admin/config/system/site-information/translate',
+ array(
+ '_controller' => '\Drupal\config_translation\Controller\ConfigTranslationController::itemPage',
+ 'plugin_id' => 'system.site_information_settings',
+ ),
+ array(
+ '_config_translation_overview_access' => 'TRUE',
+ )
+ );
+ $result = $this->configNamesMapper->getOverviewRoute();
+ $this->assertSame(serialize($expected), serialize($result));
+ }
+
+ /**
+ * Tests ConfigNamesMapper::getOverviewPath().
+ */
+ public function testGetOverviewPath() {
+ $result = $this->configNamesMapper->getOverviewPath();
+ $this->assertSame('/admin/config/system/site-information/translate', $result);
+ }
+
+ /**
+ * Tests ConfigNamesMapper::getAddRouteName().
+ */
+ public function testGetAddRouteName() {
+ $result = $this->configNamesMapper->getAddRouteName();
+ $expected = 'config_translation.item.add.' . $this->pluginDefinition['base_route_name'];
+ $this->assertSame($expected, $result);
+ }
+
+ /**
+ * Tests ConfigNamesMapper::getAddRouteParameters().
+ */
+ public function testGetAddRouteParameters() {
+ $request = Request::create('');
+ $request->attributes->set('langcode', 'xx');
+ $this->configNamesMapper->populateFromRequest($request);
+
+ $expected = array('langcode' => 'xx');
+ $result = $this->configNamesMapper->getAddRouteParameters();
+ $this->assertSame($expected, $result);
+ }
+
+ /**
+ * Tests ConfigNamesMapper::getAddRoute().
+ */
+ public function testGetAddRoute() {
+ $expected = new Route('/admin/config/system/site-information/translate/{langcode}/add',
+ array(
+ '_form' => '\Drupal\config_translation\Form\ConfigTranslationAddForm',
+ 'plugin_id' => 'system.site_information_settings',
+ ),
+ array(
+ '_config_translation_form_access' => 'TRUE',
+ )
+ );
+ $result = $this->configNamesMapper->getAddRoute();
+ $this->assertSame(serialize($expected), serialize($result));
+ }
+
+ /**
+ * Tests ConfigNamesMapper::getEditRouteName().
+ */
+ public function testGetEditRouteName() {
+ $result = $this->configNamesMapper->getEditRouteName();
+ $expected = 'config_translation.item.edit.' . $this->pluginDefinition['base_route_name'];
+ $this->assertSame($expected, $result);
+ }
+
+ /**
+ * Tests ConfigNamesMapper::getEditRouteParameters().
+ */
+ public function testGetEditRouteParameters() {
+ $request = Request::create('');
+ $request->attributes->set('langcode', 'xx');
+ $this->configNamesMapper->populateFromRequest($request);
+
+ $expected = array('langcode' => 'xx');
+ $result = $this->configNamesMapper->getEditRouteParameters();
+ $this->assertSame($expected, $result);
+ }
+
+ /**
+ * Tests ConfigNamesMapper::getEditRoute().
+ */
+ public function testGetEditRoute() {
+ $expected = new Route('/admin/config/system/site-information/translate/{langcode}/edit',
+ array(
+ '_form' => '\Drupal\config_translation\Form\ConfigTranslationEditForm',
+ 'plugin_id' => 'system.site_information_settings',
+ ),
+ array(
+ '_config_translation_form_access' => 'TRUE',
+ )
+ );
+ $result = $this->configNamesMapper->getEditRoute();
+ $this->assertSame(serialize($expected), serialize($result));
+ }
+
+ /**
+ * Tests ConfigNamesMapper::getDeleteRouteName().
+ */
+ public function testGetDeleteRouteName() {
+ $result = $this->configNamesMapper->getDeleteRouteName();
+ $expected = 'config_translation.item.delete.' . $this->pluginDefinition['base_route_name'];
+ $this->assertSame($expected, $result);
+ }
+
+ /**
+ * Tests ConfigNamesMapper::getDeleteRouteParameters().
+ */
+ public function testGetDeleteRouteParameters() {
+ $request = Request::create('');
+ $request->attributes->set('langcode', 'xx');
+ $this->configNamesMapper->populateFromRequest($request);
+
+ $expected = array('langcode' => 'xx'); $result = $this->configNamesMapper->getDeleteRouteParameters();
+ $this->assertSame($expected, $result);
+ }
+
+ /**
+ * Tests ConfigNamesMapper::getRoute().
+ */
+ public function testGetDeleteRoute() {
+ $expected = new Route('/admin/config/system/site-information/translate/{langcode}/delete',
+ array(
+ '_form' => '\Drupal\config_translation\Form\ConfigTranslationDeleteForm',
+ 'plugin_id' => 'system.site_information_settings',
+ ),
+ array(
+ '_config_translation_form_access' => 'TRUE',
+ )
+ );
+ $result = $this->configNamesMapper->getDeleteRoute();
+ $this->assertSame(serialize($expected), serialize($result));
+ }
+
+ /**
+ * Tests ConfigNamesMapper::getConfigNames().
+ */
+ public function testGetConfigNames() {
+ $result = $this->configNamesMapper->getConfigNames();
+ $this->assertSame($this->pluginDefinition['names'], $result);
+ }
+
+ /**
+ * Tests ConfigNamesMapper::addConfigName().
+ */
+ public function testAddConfigName() {
+ $names = $this->configNamesMapper->getConfigNames();
+ $this->configNamesMapper->addConfigName('test');
+ $names[] = 'test';
+ $result = $this->configNamesMapper->getConfigNames();
+ $this->assertSame($names, $result);
+ }
+
+ /**
+ * Tests ConfigNamesMapper::getWeight().
+ */
+ public function testGetWeight() {
+ $result = $this->configNamesMapper->getWeight();
+ $this->assertSame($this->pluginDefinition['weight'], $result);
+ }
+
+ /**
+ * Tests ConfigNamesMapper::populateFromRequest().
+ */
+ public function testPopulateFromRequest() {
+ // Make sure the language code is not set initially.
+ $this->assertSame(NULL, $this->configNamesMapper->getInternalLangcode());
+
+ // Test that an empty request does not set the language code.
+ $request = Request::create('');
+ $this->configNamesMapper->populateFromRequest($request);
+ $this->assertSame(NULL, $this->configNamesMapper->getInternalLangcode());
+
+ // Test that a request with a 'langcode' attribute sets the language code.
+ $request->attributes->set('langcode', 'xx');
+ $this->configNamesMapper->populateFromRequest($request);
+ $this->assertSame('xx', $this->configNamesMapper->getInternalLangcode());
+
+ // Test that the language code gets unset with the wrong request.
+ $request->attributes->remove('langcode');
+ $this->configNamesMapper->populateFromRequest($request);
+ $this->assertSame(NULL, $this->configNamesMapper->getInternalLangcode());
+ }
+
+ /**
+ * Tests ConfigNamesMapper::getTypeLabel().
+ */
+ public function testGetTypeLabel() {
+ $result = $this->configNamesMapper->getTypeLabel();
+ $this->assertSame($this->pluginDefinition['title'], $result);
+ }
+
+ /**
+ * Tests ConfigNamesMapper::getLangcode().
+ */
+ public function testGetLangcode() {
+ // Test that the getLangcode() falls back to 'en', if no explicit language
+ // code is provided.
+ $config_factory = $this->getConfigFactoryStub(array(
+ 'system.site' => array('key' => 'value'),
+ ));
+ $this->configNamesMapper->setConfigFactory($config_factory);
+ $result = $this->configNamesMapper->getLangcode();
+ $this->assertSame('en', $result);
+
+ // Test that getLangcode picks up the language code provided by the
+ // configuration.
+ $config_factory = $this->getConfigFactoryStub(array(
+ 'system.site' => array('langcode' => 'xx'),
+ ));
+ $this->configNamesMapper->setConfigFactory($config_factory);
+ $result = $this->configNamesMapper->getLangcode();
+ $this->assertSame('xx', $result);
+
+ // Test that getLangcode() works for multiple configuration names.
+ $this->configNamesMapper->addConfigName('system.maintenance');
+ $config_factory = $this->getConfigFactoryStub(array(
+ 'system.site' => array('langcode' => 'xx'),
+ 'system.maintenance' => array('langcode' => 'xx'),
+ ));
+ $this->configNamesMapper->setConfigFactory($config_factory);
+ $result = $this->configNamesMapper->getLangcode();
+ $this->assertSame('xx', $result);
+
+ // Test that getLangcode() throws an exception when different language codes
+ // are given.
+ $config_factory = $this->getConfigFactoryStub(array(
+ 'system.site' => array('langcode' => 'xx'),
+ 'system.maintenance' => array('langcode' => 'yy'),
+ ));
+ $this->configNamesMapper->setConfigFactory($config_factory);
+ try {
+ $this->configNamesMapper->getLangcode();
+ $this->fail();
+ }
+ catch (\RuntimeException $e) {}
+ }
+
+ // @todo Test ConfigNamesMapper::getLanguageWithFallback() once
+ // https://drupal.org/node/1862202 lands in core, because then we can
+ // remove the direct language_load() call.
+
+ /**
+ * Tests ConfigNamesMapper::getConfigData().
+ */
+ public function testGetConfigData() {
+ $configs = array(
+ 'system.site' => array(
+ 'name' => 'Drupal',
+ 'slogan' => 'Come for the software, stay for the community!',
+ ),
+ 'system.maintenance' => array(
+ 'enabled' => FALSE,
+ 'message' => '@site is currently under maintenance.',
+ ),
+ 'system.rss' => array(
+ 'items' => array(
+ 'limit' => 10,
+ 'view_mode' => 'rss',
+ ),
+ ),
+ );
+
+ $this->configNamesMapper->setConfigNames(array_keys($configs));
+ $config_factory = $this->getConfigFactoryStub($configs);
+ $this->configNamesMapper->setConfigFactory($config_factory);
+
+ $result = $this->configNamesMapper->getConfigData();
+ $this->assertSame($configs, $result);
+ }
+
+ /**
+ * Tests ConfigNamesMapper::hasSchema().
+ *
+ * @param array $mock_return_values
+ * An array of values that the mocked locale configuration manager should
+ * return for hasConfigSchema().
+ * @param bool $expected
+ * The expected return value of ConfigNamesMapper::hasSchema().
+ *
+ * @dataProvider providerTestHasSchema
+ */
+ public function testHasSchema(array $mock_return_values, $expected) {
+ // As the configuration names are arbitrary, simply use integers.
+ $config_names = range(1, count($mock_return_values));
+ $this->configNamesMapper->setConfigNames($config_names);
+
+ $map = array();
+ foreach ($config_names as $i => $config_name) {
+ $map[] = array($config_name, $mock_return_values[$i]);
+ }
+ $this->localeConfigManager
+ ->expects($this->any())
+ ->method('hasConfigSchema')
+ ->will($this->returnValueMap($map));
+
+ $result = $this->configNamesMapper->hasSchema();
+ $this->assertSame($expected, $result);
+ }
+
+ /**
+ * Provides data for for ConfigMapperTest::testHasSchema().
+ *
+ * @return array
+ * An array of arrays, where each inner array has an array of values that
+ * the mocked locale configuration manager should return for
+ * hasConfigSchema() as the first value and the expected return value of
+ * ConfigNamesMapper::hasSchema() as the second value.
+ */
+ public function providerTestHasSchema() {
+ return array(
+ array(array(TRUE), TRUE),
+ array(array(FALSE), FALSE),
+ array(array(TRUE, TRUE, TRUE), TRUE),
+ array(array(TRUE, FALSE, TRUE), FALSE),
+ );
+ }
+
+ /**
+ * Tests ConfigNamesMapper::hasTranslatable().
+ *
+ * @param array $mock_return_values
+ * An array of values that the mocked configuration mapper manager should
+ * return for hasTranslatable().
+ * @param bool $expected
+ * The expected return value of ConfigNamesMapper::hasTranslatable().
+ *
+ * @dataProvider providerTestHasTranslatable
+ */
+ public function testHasTranslatable(array $mock_return_values, $expected) {
+ // As the configuration names are arbitrary, simply use integers.
+ $config_names = range(1, count($mock_return_values));
+ $this->configNamesMapper->setConfigNames($config_names);
+
+ $map = array();
+ foreach ($config_names as $i => $config_name) {
+ $map[] = array($config_name, $mock_return_values[$i]);
+ }
+ $this->configMapperManager
+ ->expects($this->any())
+ ->method('hasTranslatable')
+ ->will($this->returnValueMap($map));
+
+ $result = $this->configNamesMapper->hasTranslatable();
+ $this->assertSame($expected, $result);
+ }
+
+ /**
+ * Provides data for ConfigNamesMapperTest::testHasTranslatable().
+ *
+ * @return array
+ * An array of arrays, where each inner array has an array of values that
+ * the mocked configuration mapper manager should return for
+ * hasTranslatable() as the first value and the expected return value of
+ * ConfigNamesMapper::hasTranslatable() as the second value.
+ */
+ public function providerTestHasTranslatable() {
+ return array(
+ array(array(TRUE), TRUE),
+ array(array(FALSE), FALSE),
+ array(array(TRUE, TRUE, TRUE), TRUE),
+ array(array(TRUE, FALSE, TRUE), FALSE),
+ );
+ }
+
+ /**
+ * Tests ConfigNamesMapper::hasTranslation().
+ *
+ * @param array $mock_return_values
+ * An array of values that the mocked configuration mapper manager should
+ * return for hasTranslation().
+ * @param bool $expected
+ * The expected return value of ConfigNamesMapper::hasTranslation().
+ *
+ * @dataProvider providerTestHasTranslation
+ */
+ public function testHasTranslation(array $mock_return_values, $expected) {
+ $language = new Language();
+
+ // As the configuration names are arbitrary, simply use integers.
+ $config_names = range(1, count($mock_return_values));
+ $this->configNamesMapper->setConfigNames($config_names);
+
+ $map = array();
+ foreach ($config_names as $i => $config_name) {
+ $map[] = array($config_name, $language, $mock_return_values[$i]);
+ }
+ $this->localeConfigManager
+ ->expects($this->any())
+ ->method('hasTranslation')
+ ->will($this->returnValueMap($map));
+
+ $result = $this->configNamesMapper->hasTranslation($language);
+ $this->assertSame($expected, $result);
+ }
+
+ /**
+ * Provides data for for ConfigNamesMapperTest::testHasTranslation().
+ *
+ * @return array
+ * An array of arrays, where each inner array has an array of values that
+ * the mocked configuration mapper manager should return for
+ * hasTranslation() as the first value and the expected return value of
+ * ConfigNamesMapper::hasTranslation() as the second value.
+ */
+ public function providerTestHasTranslation() {
+ return array(
+ array(array(TRUE), TRUE),
+ array(array(FALSE), FALSE),
+ array(array(TRUE, TRUE, TRUE), TRUE),
+ array(array(FALSE, FALSE, TRUE), TRUE),
+ array(array(FALSE, FALSE, FALSE), FALSE),
+ );
+ }
+
+ /**
+ * Tests ConfigNamesMapper::getTypeName().
+ */
+ public function testGetTypeName() {
+ $result = $this->configNamesMapper->getTypeName();
+ $this->assertSame('Settings', $result);
+ }
+
+ /**
+ * Tests ConfigNamesMapper::hasTranslation().
+ */
+ public function testGetOperations() {
+ $expected = array(
+ 'translate' => array(
+ 'title' => 'Translate',
+ 'href' => '/admin/config/system/site-information/translate',
+ ),
+ );
+ $result = $this->configNamesMapper->getOperations();
+ $this->assertEquals($expected, $result);
+ }
+
+}
+
+/**
+ * Defines a test mapper class.
+ */
+class TestConfigNamesMapper extends ConfigNamesMapper {
+
+ /**
+ * Gets the internal language code of this mapper, if any.
+ *
+ * This method is not to be confused with
+ * ConfigMapperInterface::getLangcode().
+ *
+ * @return string|null
+ * The language code of this mapper if it is set; NULL otherwise.
+ */
+ public function getInternalLangcode() {
+ return isset($this->langcode) ? $this->langcode : NULL;
+ }
+
+ /**
+ * Sets the list of configuration names.
+ *
+ * @param array $config_names
+ */
+ public function setConfigNames(array $config_names) {
+ $this->pluginDefinition['names'] = $config_names;
+ }
+
+ /**
+ * Sets the configuration factory.
+ *
+ * @var \Drupal\Core\Config\ConfigFactory $config_factory
+ * The config factory to set.
+ */
+ public function setConfigFactory(ConfigFactory $config_factory) {
+ $this->configFactory = $config_factory;
+ }
+
+}
diff --git a/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.info.yml b/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.info.yml
new file mode 100644
index 0000000..47e5f1f
--- /dev/null
+++ b/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.info.yml
@@ -0,0 +1,9 @@
+name: 'Configuration Translation Test'
+description: 'Helpers to test the configuration translation system'
+type: module
+package: Testing
+version: VERSION
+core: 8.x
+hidden: true
+dependencies:
+ - config_test
diff --git a/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module b/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module
new file mode 100644
index 0000000..4d651f4
--- /dev/null
+++ b/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module
@@ -0,0 +1,80 @@
+<?php
+
+/**
+ * @file
+ * Configuration Translation Test module.
+ */
+
+/**
+ * Implements hook_entity_info_alter().
+ */
+function config_translation_test_entity_info_alter(&$info) {
+ // Remove entity definition for these entity types from config_test module.
+ unset($info['config_test_no_status']);
+ unset($info['config_query_test']);
+}
+
+/**
+ * Implements hook_config_translation_info_alter().
+ */
+function config_translation_test_config_translation_info_alter(&$info) {
+ if (\Drupal::state()->get('config_translation_test_config_translation_info_alter')) {
+ // Limit account settings config files to only one of them.
+ $info['user.account_settings']['names'] = array('user.settings');
+
+ // Add one more config file to the site information page.
+ $info['system.site_information_settings']['names'][] = 'system.rss';
+ }
+}
+
+/**
+ * Implements hook_form_BASE_FORM_ID_alter() for ConfigTranslationFormBase.
+ *
+ * Adds a list of configuration names to the top of the configuration
+ * translation form.
+ *
+ * @see \Drupal\config_translation\Form\ConfigTranslationFormBase
+ */
+function config_translation_test_form_config_translation_form_alter(&$form, &$form_state) {
+ if (\Drupal::state()->get('config_translation_test_alter_form_alter')) {
+ $form['#base_altered'] = TRUE;
+ }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter() for ConfigTranslationAddForm.
+ *
+ * Changes the title to include the source language.
+ *
+ * @see \Drupal\config_translation\Form\ConfigTranslationAddForm
+ */
+function config_translation_test_form_config_translation_add_form_alter(&$form, &$form_state) {
+ if (\Drupal::state()->get('config_translation_test_alter_form_alter')) {
+ $form['#altered'] = TRUE;
+ }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter() for ConfigTranslationEditForm.
+ *
+ * Adds a column to the configuration translation edit form that shows the
+ * current translation. Note that this column would not be displayed by default,
+ * as the columns are hardcoded in
+ * config_translation_manage_form_element.html.twig. The template would need to
+ * be overridden for the column to be displayed.
+ *
+ * @see \Drupal\config_translation\Form\ConfigTranslationEditForm
+ */
+function config_translation_test_form_config_translation_edit_form_alter(&$form, &$form_state) {
+ if (\Drupal::state()->get('config_translation_test_alter_form_alter')) {
+ $form['#altered'] = TRUE;
+ }
+}
+
+/**
+ * Implements hook_system_theme_info().
+ */
+function config_translation_test_system_theme_info() {
+ $themes['config_translation_test_theme'] = drupal_get_path('module', 'config_translation') . '/tests/themes/config_translation_test_theme/config_translation_test_theme.info.yml';
+ return $themes;
+}
diff --git a/core/modules/config_translation/tests/themes/config_translation_test_theme/config_translation_test_theme.config_translation.yml b/core/modules/config_translation/tests/themes/config_translation_test_theme/config_translation_test_theme.config_translation.yml
new file mode 100644
index 0000000..7c8cdd8
--- /dev/null
+++ b/core/modules/config_translation/tests/themes/config_translation_test_theme/config_translation_test_theme.config_translation.yml
@@ -0,0 +1,5 @@
+system.performance_settings:
+ title: 'Theme translation test'
+ base_route_name: system.performance_settings
+ names:
+ - system.site
diff --git a/core/modules/config_translation/tests/themes/config_translation_test_theme/config_translation_test_theme.info.yml b/core/modules/config_translation/tests/themes/config_translation_test_theme/config_translation_test_theme.info.yml
new file mode 100644
index 0000000..3b4d655
--- /dev/null
+++ b/core/modules/config_translation/tests/themes/config_translation_test_theme/config_translation_test_theme.info.yml
@@ -0,0 +1,6 @@
+name: 'Configuration Translation Test Theme'
+type: theme
+description: 'Theme for testing the configuration translation mapper system'
+version: VERSION
+core: 8.x
+hidden: true
diff --git a/core/modules/system/system.config_translation.yml b/core/modules/system/system.config_translation.yml
new file mode 100644
index 0000000..87cc98c
--- /dev/null
+++ b/core/modules/system/system.config_translation.yml
@@ -0,0 +1,17 @@
+system.site_maintenance_mode:
+ title: 'System maintenance'
+ base_route_name: system.site_maintenance_mode
+ names:
+ - system.maintenance
+
+system.site_information_settings:
+ title: 'System information'
+ base_route_name: system.site_information_settings
+ names:
+ - system.site
+
+system.rss_feeds_settings:
+ title: 'RSS publishing'
+ base_route_name: system.rss_feeds_settings
+ names:
+ - system.rss
diff --git a/core/modules/user/user.config_translation.yml b/core/modules/user/user.config_translation.yml
new file mode 100644
index 0000000..cd3f89c
--- /dev/null
+++ b/core/modules/user/user.config_translation.yml
@@ -0,0 +1,6 @@
+user.account_settings:
+ title: 'Account settings'
+ base_route_name: user.account_settings
+ names:
+ - user.settings
+ - user.mail