diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index c695934..37f992c 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -587,3 +587,34 @@ field.float.value:
         value:
           type: float
           label: 'Value'
+
+# Human readable string that is associated with a format.
+text_format:
+  type: mapping
+  label: 'Text with text format'
+  # Even though it is not sensible to translate the text format of a formatted
+  # string, we conceive of the text and its date format as a single composite
+  # object and declare that object (or in other words the entire mapping) as
+  # translatable. This causes the entire mapping to be saved to the language
+  # overrides of the configuration. Storing only the (to be formatted) text
+  # could result in security problems in case the text format of the source
+  # text is changed.
+  translatable: true
+  mapping:
+    value:
+      type: text
+      label: 'Text'
+      # locale.module integrates the language overrides of shipped configuration
+      # with https://localize.drupal.org. Because it only handles strings and
+      # cannot deal with complex data structures, it parses the configuration
+      # schema until it reaches a primitive and only then checks whether the
+      # element is translatable.
+      translatable: true
+    format:
+      type: string
+      label: 'Text format'
+      # Even though the entire 'text_format' is marked as translatable for the
+      # sake of language configuration overrides, the ID of the text format of
+      # texts in shipped configuration should not be exposed to
+      # https://localize.drupal.org
+
diff --git a/core/lib/Drupal/Core/Config/ConfigEvents.php b/core/lib/Drupal/Core/Config/ConfigEvents.php
index 7fa22a5..2b2d2c8 100644
--- a/core/lib/Drupal/Core/Config/ConfigEvents.php
+++ b/core/lib/Drupal/Core/Config/ConfigEvents.php
@@ -9,6 +9,8 @@
 
 /**
  * Defines events for the configuration system.
+ *
+ * @see \Drupal\Core\Config\ConfigCrudEvent
  */
 final class ConfigEvents {
 
@@ -21,6 +23,18 @@
   const SAVE = 'config.save';
 
   /**
+   * Name of event fired when saving the configuration override.
+   *
+   * This event is not used by the configuration system itself but should be
+   * used by implementors of configuration overrides. See Language module's
+   * implementation for an example.
+   *
+   * @see \Drupal\Core\Config\ConfigOverrideCrudEvent
+   * @see \Drupal\language\Config\LanguageConfigOverride::save()
+   */
+  const SAVE_OVERRIDE = 'config.save_override';
+
+  /**
    * Name of event fired when deleting the configuration object.
    *
    * @see \Drupal\Core\Config\Config::delete()
@@ -28,6 +42,18 @@
   const DELETE = 'config.delete';
 
   /**
+   * Name of event fired when deleting the configuration override.
+   *
+   * This event is not used by the configuration system itself but should be
+   * used by implementors of configuration overrides. See Language module's
+   * implementation for an example.
+   *
+   * @see \Drupal\Core\Config\ConfigOverrideCrudEvent
+   * @see \Drupal\language\Config\LanguageConfigOverride::delete()
+   */
+  const DELETE_OVERRIDE = 'config.delete_override';
+
+  /**
    * Name of event fired when renaming a configuration object.
    *
    * @see \Drupal\Core\Config\ConfigFactoryInterface::rename().
diff --git a/core/lib/Drupal/Core/Config/ConfigOverrideCrudEvent.php b/core/lib/Drupal/Core/Config/ConfigOverrideCrudEvent.php
new file mode 100644
index 0000000..377221c
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/ConfigOverrideCrudEvent.php
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Config\ConfigOverrideCrudEvent.
+ */
+
+namespace Drupal\Core\Config;
+
+use Symfony\Component\EventDispatcher\Event;
+
+/**
+ * Wraps a configuration event for event listeners.
+ */
+class ConfigOverrideCrudEvent extends Event {
+
+  /**
+   * Configuration object.
+   *
+   * @var \Drupal\Core\Config\Config
+   */
+  protected $config;
+
+  /**
+   * Constructs a configuration event object.
+   *
+   * @param \Drupal\Core\Config\StorableConfigBase $config
+   *   Configuration object.
+   */
+  public function __construct(StorableConfigBase $config) {
+    $this->config = $config;
+  }
+
+  /**
+   * Gets configuration object.
+   *
+   * @return \Drupal\Core\Config\StorableConfigBase
+   *   The configuration object that caused the event to fire.
+   */
+  public function getConfig() {
+    return $this->config;
+  }
+
+}
+
diff --git a/core/lib/Drupal/Core/Config/TypedConfigManagerInterface.php b/core/lib/Drupal/Core/Config/TypedConfigManagerInterface.php
index d9ad30b..84d347b 100644
--- a/core/lib/Drupal/Core/Config/TypedConfigManagerInterface.php
+++ b/core/lib/Drupal/Core/Config/TypedConfigManagerInterface.php
@@ -24,7 +24,7 @@
    * @param string $name
    *   Configuration object name.
    *
-   * @return \Drupal\Core\Config\Schema\Element
+   * @return \Drupal\Core\TypedData\TraversableTypedDataInterface
    *   Typed configuration element.
    */
   public function get($name);
diff --git a/core/modules/config_translation/config_translation.module b/core/modules/config_translation/config_translation.module
index 32b3307..a96b434 100644
--- a/core/modules/config_translation/config_translation.module
+++ b/core/modules/config_translation/config_translation.module
@@ -173,10 +173,22 @@ function config_translation_entity_operation(EntityInterface $entity) {
  * Implements hook_config_schema_info_alter().
  */
 function config_translation_config_schema_info_alter(&$definitions) {
+  $map = array(
+    'label' => '\Drupal\config_translation\FormElement\Textfield',
+    'text' => '\Drupal\config_translation\FormElement\Textarea',
+    'date_format' => '\Drupal\config_translation\FormElement\DateFormat',
+    'text_format' => '\Drupal\config_translation\FormElement\TextFormat',
+    'mapping' => '\Drupal\config_translation\FormElement\ListElement',
+    'sequence' => '\Drupal\config_translation\FormElement\ListElement',
+  );
+
   // 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';
+  foreach ($definitions as $type => &$definition) {
+    if (!isset($definition['form_element_class']) && isset($map[$type])) {
+      $definition['form_element_class'] = $map[$type];
+    }
+  }
 }
 
diff --git a/core/modules/config_translation/src/Form/ConfigTranslationFormBase.php b/core/modules/config_translation/src/Form/ConfigTranslationFormBase.php
index fefe5f8..98fd84e 100644
--- a/core/modules/config_translation/src/Form/ConfigTranslationFormBase.php
+++ b/core/modules/config_translation/src/Form/ConfigTranslationFormBase.php
@@ -8,17 +8,12 @@
 namespace Drupal\config_translation\Form;
 
 use Drupal\config_translation\ConfigMapperManagerInterface;
-use Drupal\Core\Config\Config;
-use Drupal\Core\Config\Schema\Element;
 use Drupal\Core\Config\TypedConfigManagerInterface;
-use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\TypedData\TypedDataInterface;
 use Drupal\Core\Form\BaseFormIdInterface;
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Language\LanguageInterface;
-use Drupal\language\Config\LanguageConfigOverride;
 use Drupal\language\ConfigurableLanguageManagerInterface;
-use Drupal\locale\StringStorageInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@@ -43,13 +38,6 @@
   protected $configMapperManager;
 
   /**
-   * The string translation storage object.
-   *
-   * @var \Drupal\locale\StringStorageInterface
-   */
-  protected $localeStorage;
-
-  /**
    * The mapper for configuration translation.
    *
    * @var \Drupal\config_translation\ConfigMapperInterface
@@ -85,19 +73,18 @@
   protected $baseConfigData = array();
 
   /**
-   * Creates manage form object with string translation storage.
+   * Constructs a ConfigTranslationFormBase.
    *
    * @param \Drupal\Core\Config\TypedConfigManagerInterface $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\language\ConfigurableLanguageManagerInterface $language_manager
+   *   The configurable language manager.
    */
-  public function __construct(TypedConfigManagerInterface $typed_config_manager, ConfigMapperManagerInterface $config_mapper_manager, StringStorageInterface $locale_storage, ConfigurableLanguageManagerInterface $language_manager) {
+  public function __construct(TypedConfigManagerInterface $typed_config_manager, ConfigMapperManagerInterface $config_mapper_manager, ConfigurableLanguageManagerInterface $language_manager) {
     $this->typedConfigManager = $typed_config_manager;
     $this->configMapperManager = $config_mapper_manager;
-    $this->localeStorage = $locale_storage;
     $this->languageManager = $language_manager;
   }
 
@@ -108,7 +95,6 @@ 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('language_manager')
     );
   }
@@ -179,13 +165,21 @@ public function buildForm(array $form, FormStateInterface $form_state, Request $
 
     $form['#attached']['library'][] = 'config_translation/drupal.config_translation.admin';
 
-    $form['config_names'] = array(
-      '#type' => 'container',
-      '#tree' => TRUE,
-    );
+    // Even though this is a nested form, we do not set #tree to TRUE because
+    // the form value structure is generated by using #parents for each element.
+    // @see \Drupal\config_translation\FormElement\FormElementBase::getElements()
+    $form['config_names'] = array('#type' => 'container');
     foreach ($this->mapper->getConfigNames() as $name) {
       $form['config_names'][$name] = array('#type' => 'container');
-      $form['config_names'][$name] += $this->buildConfigForm($this->typedConfigManager->get($name), $config_factory->get($name)->get(), $this->baseConfigData[$name]);
+
+      $schema = $this->typedConfigManager->get($name);
+      $source_config = $this->baseConfigData[$name];
+      $translation_config = $config_factory->get($name)->get();
+
+      if ($form_element = $this->createFormElement($schema)) {
+        $parents = array('config_names', $name);
+        $form['config_names'][$name] += $form_element->getTranslationBuild($this->sourceLanguage, $this->language, $source_config, $translation_config, $parents);
+      }
     }
 
     $form['actions']['#type'] = 'actions';
@@ -205,7 +199,7 @@ public function buildForm(array $form, FormStateInterface $form_state, Request $
    * {@inheritdoc}
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
-    $form_values = $form_state->getValue('config_names');
+    $form_values = $form_state->getValue(array('translation', 'config_names'));
 
     // For the form submission handling, use the raw data.
     $config_factory = $this->configFactory();
@@ -213,12 +207,14 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
     $config_factory->setOverrideState(FALSE);
 
     foreach ($this->mapper->getConfigNames() as $name) {
+      $schema = $this->typedConfigManager->get($name);
+
       // Set configuration values based on form submission and source values.
       $base_config = $config_factory->get($name);
       $config_translation = $this->languageManager->getLanguageConfigOverride($this->language->getId(), $name);
-      $locations = $this->localeStorage->getLocations(array('type' => 'configuration', 'name' => $name));
 
-      $this->setConfig($this->language, $base_config, $config_translation, $form_values[$name], !empty($locations));
+      $element = $this->createFormElement($schema);
+      $element->setConfig($base_config, $config_translation, $form_values[$name]);
 
       // If no overrides, delete language specific configuration file.
       $saved_config = $config_translation->get();
@@ -238,169 +234,25 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
   }
 
   /**
-   * Formats configuration schema as a form tree.
+   * Create form element builder.
    *
-   * @param \Drupal\Core\Config\Schema\Element $schema
+   * @param \Drupal\Core\TypedData\TypedDataInterface $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 $open
-   *   (optional) Whether or not the details element of the form should be open.
-   *   Defaults to TRUE.
-   * @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.
+   * @return \Drupal\config_translation\FormElement\ElementInterface|null
+   *   The element builder object if possible.
    */
-  protected function buildConfigForm(Element $schema, $config_data, $base_config_data, $open = TRUE, $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->getDataDefinition();
+  public static function createFormElement(TypedDataInterface $schema) {
+    $definition = $schema->getDataDefinition();
+    // Form element classes can be specified even for non-translatable elements
+    // such as the ListElement form element which used for for Mapping and List
+    // schema elements.
+    if (isset($definition['form_element_class'])) {
       if (!$definition->getLabel()) {
-        $definition->setLabel($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], FALSE, $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']),
-            '#open' => $open,
-          ) + $sub_build;
-        }
-      }
-      else {
-        $definition = $element->getDataDefinition();
-
-        // 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->getId() . '">' . 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',
-        );
-
-        if (!isset($definition['form_element_class'])) {
-          $definition['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\LanguageInterface $language
-   *   Set the configuration in this language.
-   * @param \Drupal\Core\Config\Config $base_config
-   *   Base configuration values, in the source language.
-   * @param \Drupal\language\Config\LanguageConfigOverride $config_translation
-   *   Translation configuration override data.
-   * @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
-   *   (optional) Flag to specify whether the configuration had a shipped
-   *   version and therefore should also be stored in the locale database.
-   *
-   * @return array
-   *   Translation configuration override data.
-   */
-  protected function setConfig(LanguageInterface $language, Config $base_config, LanguageConfigOverride $config_translation, 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, $config_translation, $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->getId(),
-          );
-          $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']) {
-          $config_translation->set($key, $value['translation']);
-        }
-        else {
-          $config_translation->clear($key);
-        }
+        $definition->setLabel(t('n/a'));
       }
+      $class = $definition['form_element_class'];
+      return $class::create($schema);
     }
   }
 
diff --git a/core/modules/config_translation/src/FormElement/DateFormat.php b/core/modules/config_translation/src/FormElement/DateFormat.php
index 5b19e26..729afee 100644
--- a/core/modules/config_translation/src/FormElement/DateFormat.php
+++ b/core/modules/config_translation/src/FormElement/DateFormat.php
@@ -12,34 +12,29 @@
 use Drupal\Core\Ajax\ReplaceCommand;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Language\LanguageInterface;
-use Drupal\Core\StringTranslation\StringTranslationTrait;
-use Drupal\Core\TypedData\DataDefinitionInterface;
 
 /**
  * Defines the date format element for the configuration translation interface.
  */
-class DateFormat implements ElementInterface {
-  use StringTranslationTrait;
+class DateFormat extends FormElementBase {
 
   /**
    * {@inheritdoc}
    */
-  public function getFormElement(DataDefinitionInterface $definition, LanguageInterface $language, $value) {
+  public function getTranslationElement(LanguageInterface $translation_language, $source_config, $translation_config) {
     $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.formatter')->format(REQUEST_TIME, 'custom', $value)));
+    $format = $this->t('Displayed as %date_format', array('%date_format' => \Drupal::service('date.formatter')->format(REQUEST_TIME, 'custom', $translation_config)));
+
     return array(
       '#type' => 'textfield',
-      '#title' => $this->t($definition->getLabel()) . '<span class="visually-hidden"> (' . $language->name . ')</span>',
       '#description' => $description,
-      '#default_value' => $value,
-      '#attributes' => array('lang' => $language->getId()),
       '#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),
       ),
-    );
+    ) + parent::getTranslationElement($translation_language, $source_config, $translation_config);
   }
 
   /**
diff --git a/core/modules/config_translation/src/FormElement/ElementInterface.php b/core/modules/config_translation/src/FormElement/ElementInterface.php
index 418860f..dd660b6 100644
--- a/core/modules/config_translation/src/FormElement/ElementInterface.php
+++ b/core/modules/config_translation/src/FormElement/ElementInterface.php
@@ -7,8 +7,10 @@
 
 namespace Drupal\config_translation\FormElement;
 
+use Drupal\Core\Config\Config;
 use Drupal\Core\Language\LanguageInterface;
-use Drupal\Core\TypedData\DataDefinitionInterface;
+use Drupal\Core\TypedData\TypedDataInterface;
+use Drupal\language\Config\LanguageConfigOverride;
 
 /**
  * Provides an interface for configuration translation form elements.
@@ -16,19 +18,56 @@
 interface ElementInterface {
 
   /**
-   * Returns the translation form element for a given configuration definition.
+   * Creates a form element instance from a schema definition.
    *
-   * @param \Drupal\Core\TypedData\DataDefinitionInterface $definition
-   *   Configuration schema type definition of the element.
-   * @param \Drupal\Core\Language\LanguageInterface $language
-   *   Language object to display the translation form for.
-   * @param string $value
-   *   Default value for the form element.
+   * @param \Drupal\Core\TypedData\TypedDataInterface $schema
+   *   The configuration schema.
+   *
+   * @return static
+   */
+  public static function create(TypedDataInterface $schema);
+
+  /**
+   * Returns the source and translation elements for a given configuration
+   * definition.
+   *
+   * @param \Drupal\Core\Language\LanguageInterface $source_language
+   *   Thee source language of the configuration object.
+   * @param \Drupal\Core\Language\LanguageInterface $translation_language
+   *   The language to display the translation form for.
+   * @param mixed $source_config
+   *   The configuration value of the element in the source language.
+   * @param mixed $translation_config
+   *   The configuration value of the element in the language to translate to.
+   * @param array $parents
+   *   Parents array for the element in the form.
+   * @param string $base_key
+   *   (optional) Base key to be used for the elements in the form. NULL for
+   *   top-level form elements.
    *
    * @return array
-   *   Form API array to represent the form element.
+   *   A render array for the source value.
    */
-  public function getFormElement(DataDefinitionInterface $definition, LanguageInterface $language, $value);
+  public function getTranslationBuild(LanguageInterface $source_language, LanguageInterface $translation_language, $source_config, $translation_config, $parents, $base_key = NULL);
 
+  /**
+   * Sets configuration based on a nested form value array.
+   *
+   * @param \Drupal\Core\Config\Config $base_config
+   *   Base configuration values, in the source language.
+   * @param \Drupal\language\Config\LanguageConfigOverride $config_translation
+   *   Translation configuration override data.
+   * @param mixed $config_values
+   *   The configuration value of the element taken from the form values.
+   * @param string|null $base_key
+   *   (optional) The base key that the schema and the configuration values
+   *   belong to. This should be NULL for the top-level configuration object and
+   *   be populated consecutively when recursing into the configuration
+   *   structure.
+   *
+   * @return array
+   *   Translation configuration override data.
+   */
+  public function setConfig(Config $base_config, LanguageConfigOverride $config_translation, $config_values, $base_key = NULL);
 
 }
diff --git a/core/modules/config_translation/src/FormElement/FormElementBase.php b/core/modules/config_translation/src/FormElement/FormElementBase.php
new file mode 100644
index 0000000..88d8f83
--- /dev/null
+++ b/core/modules/config_translation/src/FormElement/FormElementBase.php
@@ -0,0 +1,190 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\FormElement\FormElementBase.
+ */
+
+namespace Drupal\config_translation\FormElement;
+
+use Drupal\Core\Config\Config;
+use Drupal\Core\Config\Schema\Element;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\TypedData\DataDefinitionInterface;
+use Drupal\Core\TypedData\TypedDataInterface;
+use Drupal\language\Config\LanguageConfigOverride;
+
+/**
+ * Provides a common base class for form elements.
+ */
+abstract class FormElementBase implements ElementInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The schema element this form is for.
+   *
+   * @var \Drupal\Core\TypedData\TypedDataInterface
+   */
+  protected $element;
+
+  /**
+   * The data definition of the element this form element is for.
+   *
+   * @var \Drupal\Core\TypedData\DataDefinitionInterface
+   */
+  protected $definition;
+
+  /**
+   * Constructs a FormElementBase.
+   *
+   * @param \Drupal\Core\TypedData\TypedDataInterface $element
+   *   The schema element this form element is for.
+   */
+  public function __construct(TypedDataInterface $element) {
+    $this->element = $element;
+    $this->definition = $element->getDataDefinition();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(TypedDataInterface $schema) {
+    return new static($schema);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTranslationBuild(LanguageInterface $source_language, LanguageInterface $translation_language, $source_config, $translation_config, $parents, $base_key = NULL) {
+    $build['#theme'] = 'config_translation_manage_form_element';
+
+    // For accessibility we make source and translation appear next to each
+    // other in the source for each element, which is why we utilize the
+    // 'source' and 'translation' sub-keys for the form. The form values,
+    // however, should mirror the configuration structure, so that we can
+    // traverse the configuration schema and still access the right
+    // configuration values in ConfigTranslationFormBase::setConfig().
+    // Therefore we make the 'source' and 'translation' keys the top-level
+    // keys in $form_state['values'].
+    $build['source'] = $this->getSourceElement($source_language, $source_config);
+    $build['translation'] = $this->getTranslationElement($translation_language, $source_config, $translation_config);
+
+    $build['source']['#parents'] = array_merge(array('source'), $parents);
+    $build['translation']['#parents'] = array_merge(array('translation'), $parents);
+    return $build;
+  }
+
+  /**
+   * Returns the source element for a given configuration definition.
+   *
+   * This can be either a render array that actually outputs the source values
+   * directly or a read-only form element with the source values depending on
+   * what is considered to provide a more intuitive user interface for the
+   * translator.
+   *
+   * @param \Drupal\Core\Language\LanguageInterface $source_language
+   *   Thee source language of the configuration object.
+   * @param mixed $source_config
+   *   The configuration value of the element in the source language.
+   *
+   * @return array
+   *   A render array for the source value.
+   */
+  protected function getSourceElement(LanguageInterface $source_language, $source_config) {
+    if ($source_config) {
+      $value = '<span lang="' . $source_language->getId() . '">' . nl2br($source_config) . '</span>';
+    }
+    else {
+      $value = $this->t('(Empty)');
+    }
+
+    return array(
+      '#type' => 'item',
+      '#title' => $this->t('!label <span class="visually-hidden">(!source_language)</span>', array(
+        '!label' => $this->t($this->definition->getLabel()),
+        '!source_language' => $source_language->getName(),
+      )),
+      '#markup' => $value,
+    );
+  }
+
+  /**
+   * Returns the translation form element for a given configuration definition.
+   *
+   * For complex data structures (such as mappings) that are translatable
+   * wholesale but contain non-translatable properties, the form element is
+   * responsible for checking access to the source value of those properties. In
+   * case of formatted text, for example, access to the source text format must
+   * be checked. If the translator does not have access to the text format, the
+   * textarea must be disabled and the translator may not be able to translate
+   * this particular configuration element. If the translator does have access
+   * to the text format, the element must be locked down to that particular text
+   * format; in other words, the format may not be changed by the translator
+   * (because the text format property is not itself translatable).
+   *
+   * In addition, the form element is responsible for checking whether the
+   * value of such non-translatable properties in the translated configuration
+   * is equal to the corresponding source values. If not, that means that the
+   * source value has changed after the translation was added. In this case -
+   * again - the translation of this element must be disabled if the translator
+   * does not have access to the source value of the non-translatable property.
+   * For example, if a formatted text element, whose source format was plain
+   * text when it was first translated, gets changed to the full HTML format,
+   * simply changing the format of the translation would lead to an XSS
+   * vulnerability as the translated text, that was intended to be escaped,
+   * would now be displayed unescaped. Thus, if the translator does not have
+   * access to the Full HTML format, the translation for this particular element
+   * may not be updated at all (the textarea must be disabled). Only if access
+   * to the Full HTML format is granted, an explicit translation taking into
+   * account the updated source value(s) may be submitted.
+   *
+   * In the specific case of formatted text this logic is implemented by
+   * utilizing a form element of type 'text_format' and its #format and
+   * #allowed_formats properties. The access logic explained above is then
+   * handled by the 'text_format' element itself, specifically by
+   * filter_process_format(). In case such a rich element is not available for
+   * translation of complex data, similar access logic must be implemented
+   * manually.
+   *
+   * @param \Drupal\Core\Language\LanguageInterface $translation_language
+   *   The language to display the translation form for.
+   * @param mixed $source_config
+   *   The configuration value of the element in the source language.
+   * @param mixed $translation_config
+   *   The configuration value of the element in the language to translate to.
+   *
+   * @return array
+   *   Form API array to represent the form element.
+   *
+   * @see \Drupal\config_translation\FormElement\TextFormat
+   * @see filter_process_format()
+   */
+  protected function getTranslationElement(LanguageInterface $translation_language, $source_config, $translation_config) {
+    // Add basic properties that apply to all form elements.
+    return array(
+      '#title' => $this->t('!label <span class="visually-hidden">(!source_language)</span>', array(
+        '!label' => $this->t($this->definition['label']),
+        '!source_language' => $translation_language->getName(),
+      )),
+      '#default_value' => $translation_config,
+      '#attributes' => array('lang' => $translation_language->getId()),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setConfig(Config $base_config, LanguageConfigOverride $config_translation, $config_values, $base_key = NULL) {
+      // Save value, if different from the source value in the base
+      // configuration. If same as original configuration, remove override.
+      if ($base_config->get($base_key) !== $config_values) {
+        $config_translation->set($base_key, $config_values);
+      }
+      else {
+        $config_translation->clear($base_key);
+      }
+    }
+
+}
diff --git a/core/modules/config_translation/src/FormElement/ListElement.php b/core/modules/config_translation/src/FormElement/ListElement.php
new file mode 100644
index 0000000..3481a0f
--- /dev/null
+++ b/core/modules/config_translation/src/FormElement/ListElement.php
@@ -0,0 +1,143 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\FormElement\ListElement.
+ */
+
+namespace Drupal\config_translation\FormElement;
+
+use Drupal\Core\Config\Config;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\config_translation\Form\ConfigTranslationFormBase;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\TypedData\DataDefinitionInterface;
+use Drupal\Core\TypedData\TraversableTypedDataInterface;
+use Drupal\Core\TypedData\TypedDataInterface;
+use Drupal\language\Config\LanguageConfigOverride;
+
+/**
+ * Defines the list element for the configuration translation interface.
+ */
+class ListElement implements ElementInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * The schema element this form is for.
+   *
+   * @var \Drupal\Core\TypedData\TraversableTypedDataInterface
+   */
+  protected $element;
+
+  /**
+   * Constructs a ListElement.
+   *
+   * @param \Drupal\Core\TypedData\TraversableTypedDataInterface $element
+   *   The schema element this form element is for.
+   */
+  public function __construct(TraversableTypedDataInterface $element) {
+    $this->element = $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(TypedDataInterface $schema) {
+    return new static($schema);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTranslationBuild(LanguageInterface $source_language, LanguageInterface $translation_language, $source_config, $translation_config, $parents, $base_key = NULL) {
+    $build = array();
+    foreach ($this->element as $key => $element) {
+      $sub_build = array();
+      // Make the specific element key, "$base_key.$key".
+      $element_key = implode('.', array_filter(array($base_key, $key)));
+      $definition = $element->getDataDefinition();
+
+      if ($form_element = ConfigTranslationFormBase::createFormElement($element)) {
+        $element_parents = array_merge($parents, array($key));
+        $sub_build += $form_element->getTranslationBuild($source_language, $translation_language, $source_config[$key], $translation_config[$key], $element_parents, $element_key);
+
+        if (empty($sub_build)) {
+          continue;
+        }
+
+        // Build sub-structure and include it with a wrapper in the form
+        // if there are any translatable elements there.
+        $build[$key] = array();
+        if ($element instanceof TraversableTypedDataInterface) {
+          $build[$key] = array(
+            '#type' => 'details',
+            '#title' => $this->getGroupTitle($definition, $sub_build),
+            '#open' => empty($base_key),
+          );
+        }
+        $build[$key] += $sub_build;
+      }
+    }
+
+    return $build;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setConfig(Config $base_config, LanguageConfigOverride $config_translation, $config_values, $base_key = NULL) {
+    foreach ($this->element as $key => $element) {
+      // Do not bother traversing schema elements for which no values have been
+      // submitted.
+      if (!isset($config_values[$key])) {
+        continue;
+      }
+      $value = $config_values[$key];
+
+      $element_key = implode('.', array_filter(array($base_key, $key)));
+      if ($form_element = ConfigTranslationFormBase::createFormElement($element)) {
+        // Traverse into the next level of the configuration.
+        $form_element->setConfig($base_config, $config_translation, $value, $element_key);
+      }
+    }
+  }
+
+  /**
+   * Returns the title for the 'details' element of a group of schema elements.
+   *
+   * For some configuration elements the same element structure can be repeated
+   * multiple times (for example views displays, filters, etc.). Thus, we try to
+   * find a more usable title for the details summary. First check if there is
+   * an element which is called title or label and use its value, then check if
+   * there is an element which contains these words and use those. Fall back
+   * to the generic definition label if no such element is found.
+   *
+   * @param \Drupal\Core\TypedData\DataDefinitionInterface $definition
+   *   The defintion of the schema element.
+   * @param $group_build
+   *   The renderable array for the group of schema elements.
+   *
+   * @return string
+   *   The title for the group of schema elements.
+   */
+  protected function getGroupTitle(DataDefinitionInterface $definition, $group_build) {
+    $title = '';
+    if (isset($group_build['title']['source'])) {
+      $title = $group_build['title']['source']['#markup'];
+    }
+    elseif (isset($group_build['label']['source'])) {
+      $title = $group_build['label']['source']['#markup'];
+    }
+    else {
+      foreach (array_keys($group_build) as $title_key) {
+        if (isset($group_build[$title_key]['source']) && (strpos($title_key, 'title') !== FALSE || strpos($title_key, 'label') !== FALSE)) {
+          $title = $group_build[$title_key]['source']['#markup'];
+          break;
+        }
+      }
+    }
+    return (!empty($title) ? (strip_tags($title) . ' ') : '') . $this->t($definition['label']);
+  }
+
+}
diff --git a/core/modules/config_translation/src/FormElement/TextFormat.php b/core/modules/config_translation/src/FormElement/TextFormat.php
new file mode 100644
index 0000000..5948085
--- /dev/null
+++ b/core/modules/config_translation/src/FormElement/TextFormat.php
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\FormElement\TextFormat.
+ */
+
+namespace Drupal\config_translation\FormElement;
+
+use Drupal\Core\Language\LanguageInterface;
+
+/**
+ * Defines the text_format element for the configuration translation interface.
+ */
+class TextFormat extends FormElementBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSourceElement(LanguageInterface $source_language, $source_config) {
+    // Instead of the formatted output show a disabled textarea. This allows for
+    // easier side-by-side comparison, especially with formats with text
+    // editors.
+    return $this->getTranslationElement($source_language, $source_config, $source_config) + array(
+      '#value' => $source_config['value'],
+      '#disabled' => TRUE,
+      '#allow_focus' => TRUE,
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTranslationElement(LanguageInterface $translation_language, $source_config, $translation_config) {
+    return array(
+      '#type' => 'text_format',
+      // Override the #default_value property from the parent class.
+      '#default_value' => $translation_config['value'],
+      '#format' => $translation_config['format'],
+      // @see \Drupal\config_translation\Element\ElementInterface::getTranslationElement()
+      '#allowed_formats' => array($source_config['format']),
+    ) + parent::getTranslationElement($translation_language, $source_config, $translation_config);
+  }
+
+}
diff --git a/core/modules/config_translation/src/FormElement/Textarea.php b/core/modules/config_translation/src/FormElement/Textarea.php
index 05b18bb..17ee1a2 100644
--- a/core/modules/config_translation/src/FormElement/Textarea.php
+++ b/core/modules/config_translation/src/FormElement/Textarea.php
@@ -8,31 +8,25 @@
 namespace Drupal\config_translation\FormElement;
 
 use Drupal\Core\Language\LanguageInterface;
-use Drupal\Core\StringTranslation\StringTranslationTrait;
-use Drupal\Core\TypedData\DataDefinitionInterface;
 
 /**
  * Defines the textarea element for the configuration translation interface.
  */
-class Textarea implements ElementInterface {
-  use StringTranslationTrait;
+class Textarea extends FormElementBase {
 
   /**
    * {@inheritdoc}
    */
-  public function getFormElement(DataDefinitionInterface $definition, LanguageInterface $language, $value) {
+  public function getTranslationElement(LanguageInterface $translation_language, $source_config, $translation_config) {
     // Estimate a comfortable size of the input textarea.
-    $rows_words = ceil(str_word_count($value) / 5);
-    $rows_newlines = substr_count($value, "\n" ) + 1;
+    $rows_words = ceil(str_word_count($translation_config) / 5);
+    $rows_newlines = substr_count($translation_config, "\n" ) + 1;
     $rows = max($rows_words, $rows_newlines);
 
     return array(
       '#type' => 'textarea',
-      '#default_value' => $value,
-      '#title' => $this->t($definition->getLabel()) . '<span class="visually-hidden"> (' . $language->name . ')</span>',
       '#rows' => $rows,
-      '#attributes' => array('lang' => $language->getId()),
-    );
+    ) + parent::getTranslationElement($translation_language, $source_config, $translation_config);
   }
 
 }
diff --git a/core/modules/config_translation/src/FormElement/Textfield.php b/core/modules/config_translation/src/FormElement/Textfield.php
index ccd7cc5..87632f4 100644
--- a/core/modules/config_translation/src/FormElement/Textfield.php
+++ b/core/modules/config_translation/src/FormElement/Textfield.php
@@ -8,25 +8,19 @@
 namespace Drupal\config_translation\FormElement;
 
 use Drupal\Core\Language\LanguageInterface;
-use Drupal\Core\StringTranslation\StringTranslationTrait;
-use Drupal\Core\TypedData\DataDefinitionInterface;
 
 /**
  * Defines the textfield element for the configuration translation interface.
  */
-class Textfield implements ElementInterface {
-  use StringTranslationTrait;
+class Textfield extends FormElementBase {
 
   /**
    * {@inheritdoc}
    */
-  public function getFormElement(DataDefinitionInterface $definition, LanguageInterface $language, $value) {
+  public function getTranslationElement(LanguageInterface $translation_language, $source_config, $translation_config) {
     return array(
       '#type' => 'textfield',
-      '#default_value' => $value,
-      '#title' => $this->t($definition->getLabel()) . '<span class="visually-hidden"> (' . $language->name . ')</span>',
-      '#attributes' => array('lang' => $language->getId()),
-    );
+    ) + parent::getTranslationElement($translation_language, $source_config, $translation_config);
   }
 
 }
diff --git a/core/modules/config_translation/src/Tests/ConfigTranslationFormTest.php b/core/modules/config_translation/src/Tests/ConfigTranslationFormTest.php
index da049df..e747bd2 100644
--- a/core/modules/config_translation/src/Tests/ConfigTranslationFormTest.php
+++ b/core/modules/config_translation/src/Tests/ConfigTranslationFormTest.php
@@ -22,7 +22,7 @@ class ConfigTranslationFormTest extends WebTestBase {
    *
    * @var array
    */
-  public static $modules = array('config_translation', 'config_translation_test');
+  public static $modules = array('config_translation', 'config_translation_test', 'editor');
 
   /**
    * The plugin ID of the mapper to test.
diff --git a/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php b/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php
index 690beee..d33a565 100644
--- a/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php
+++ b/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\config_translation\Tests;
 
 use Drupal\Component\Serialization\Json;
+use Drupal\Component\Utility\String;
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Config\FileStorage;
 use Drupal\Core\Language\Language;
@@ -27,7 +28,7 @@ class ConfigTranslationUiTest extends WebTestBase {
    *
    * @var array
    */
-  public static $modules = array('node', 'contact', 'config_translation', 'config_translation_test', 'views', 'views_ui', 'contextual');
+  public static $modules = array('node', 'contact', 'config_translation', 'config_translation_test', 'views', 'views_ui', 'contextual', 'filter', 'filter_test');
 
   /**
    * Languages to enable.
@@ -62,12 +63,24 @@ protected function setUp() {
     $translator_permissions = array(
       'translate configuration',
     );
+
+    /** @var \Drupal\filter\FilterFormatInterface $filter_test_format */
+    $filter_test_format = entity_load('filter_format', 'filter_test');
+    /** @var \Drupal\filter\FilterFormatInterface $filtered_html_format */
+    $filtered_html_format = entity_load('filter_format', 'filtered_html');
+    /** @var \Drupal\filter\FilterFormatInterface $full_html_format */
+    $full_html_format = entity_load('filter_format', 'full_html');
+
     $admin_permissions = array_merge(
       $translator_permissions,
       array(
         'administer languages',
         'administer site configuration',
         'administer contact forms',
+        'administer filters',
+        $filtered_html_format->getPermissionName(),
+        $full_html_format->getPermissionName(),
+        $filter_test_format->getPermissionName(),
         'access site-wide contact form',
         'access contextual links',
         'administer views',
@@ -122,8 +135,8 @@ public function testSiteInformationTranslationUi() {
 
     // 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,
+      'translation[config_names][system.site][name]' => $fr_site_name,
+      'translation[config_names][system.site][slogan]' => $fr_site_slogan,
     );
 
     $this->drupalPostForm("$translation_base_url/fr/add", $edit, t('Save translation'));
@@ -136,8 +149,8 @@ public function testSiteInformationTranslationUi() {
 
     // 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);
+    $this->assertFieldByName('translation[config_names][system.site][name]', $fr_site_name);
+    $this->assertFieldByName('translation[config_names][system.site][slogan]', $fr_site_slogan);
 
     // Check French translation of site name and slogan are in place.
     $this->drupalGet('fr');
@@ -165,8 +178,8 @@ public function testSourceValueDuplicateSave() {
 
     // 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,
+      'translation[config_names][system.site][name]' => 'FR ' . $site_name,
+      'translation[config_names][system.site][slogan]' => 'FR ' . $site_slogan,
     );
     // First time, no overrides, so just Add link.
     $this->drupalPostForm("$translation_base_url/fr/add", $edit, t('Save translation'));
@@ -188,8 +201,8 @@ public function testSourceValueDuplicateSave() {
     $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,
+      'translation[config_names][system.site][name]' => $site_name,
+      'translation[config_names][system.site][slogan]' => 'FR ' . $site_slogan,
     );
     $this->drupalPostForm(NULL, $edit, t('Save translation'));
     $this->assertRaw(t('Successfully updated @language translation.', array('@language' => 'French')));
@@ -203,8 +216,8 @@ public function testSourceValueDuplicateSave() {
     $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,
+      'translation[config_names][system.site][name]' => $site_name,
+      'translation[config_names][system.site][slogan]' => $site_slogan,
     );
     $this->drupalPostForm(NULL, $edit, t('Save translation'));
     $override = \Drupal::languageManager()->getLanguageConfigOverride('fr', 'system.site');
@@ -268,8 +281,8 @@ public function testContactConfigEntityTranslation() {
 
       // Update translatable fields.
       $edit = array(
-        'config_names[contact.form.feedback][label][translation]' => 'Website feedback - ' . $langcode,
-        'config_names[contact.form.feedback][reply][translation]' => 'Thank you for your mail - ' . $langcode,
+        'translation[config_names][contact.form.feedback][label]' => 'Website feedback - ' . $langcode,
+        'translation[config_names][contact.form.feedback][reply]' => 'Thank you for your mail - ' . $langcode,
       );
 
       // Save language specific version of form.
@@ -307,7 +320,7 @@ public function testContactConfigEntityTranslation() {
       $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.form.feedback][label][translation]', 'Website feedback - ' . $langcode);
+        $this->assertFieldByName('translation[config_names][contact.form.feedback][label]', 'Website feedback - ' . $langcode);
         $this->assertText($label);
       }
     }
@@ -397,8 +410,8 @@ public function testDateFormatTranslation() {
 
       // Update translatable fields.
       $edit = array(
-        'config_names[core.date_format.' . $id . '][label][translation]' => $id . ' - FR',
-        'config_names[core.date_format.' . $id . '][pattern][translation]' => 'D',
+        'translation[config_names][core.date_format.' . $id . '][label]' => $id . ' - FR',
+        'translation[config_names][core.date_format.' . $id . '][pattern]' => 'D',
       );
 
       // Save language specific version of form.
@@ -434,9 +447,9 @@ public function testAccountSettingsConfigurationTranslation() {
 
     // 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.',
+      'translation[config_names][user.settings][anonymous]' => 'Anonyme',
+      'translation[config_names][user.mail][status_blocked][subject]' => 'Testing, your account is blocked.',
+      'translation[config_names][user.mail][status_blocked][body]' => 'Testing account blocked body.',
     );
 
     $this->drupalPostForm('admin/config/people/accounts/translate/fr/add', $edit, t('Save translation'));
@@ -445,7 +458,7 @@ public function testAccountSettingsConfigurationTranslation() {
     $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 . '"]';
+      $xpath = '//' . (strpos($key, '[body]') ? 'textarea' : 'input') . '[@name="'. $key . '"]';
       $this->assertFieldByXPath($xpath, $value);
     }
     // Check that labels for email settings appear.
@@ -532,10 +545,10 @@ public function testViewsTranslationUI() {
 
     // 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",
+      'translation[config_names][views.view.frontpage][description]' => $description . " FR",
+      'translation[config_names][views.view.frontpage][label]' => $human_readable_name . " FR",
+      'translation[config_names][views.view.frontpage][display][default][display_title]' => $display_settings_master . " FR",
+      'translation[config_names][views.view.frontpage][display][default][display_options][title]' => $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')));
@@ -547,10 +560,10 @@ public function testViewsTranslationUI() {
 
     // 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");
+    $this->assertFieldByName('translation[config_names][views.view.frontpage][description]', $description . " FR");
+    $this->assertFieldByName('translation[config_names][views.view.frontpage][label]', $human_readable_name . " FR");
+    $this->assertFieldByName('translation[config_names][views.view.frontpage][display][default][display_title]', $display_settings_master . " FR");
+    $this->assertFieldByName('translation[config_names][views.view.frontpage][display][default][display_options][title]', $display_options_master . " FR");
   }
 
   /**
@@ -581,7 +594,7 @@ public function testLocaleDBStorage() {
 
     // Add custom translation.
     $edit = array(
-      'config_names[user.settings][anonymous][translation]' => 'Anonyme',
+      'translation[config_names][user.settings][anonymous]' => 'Anonyme',
     );
     $this->drupalPostForm('admin/config/people/accounts/translate/fr/add', $edit, t('Save translation'));
 
@@ -592,7 +605,7 @@ public function testLocaleDBStorage() {
 
     // revert custom translations to base translation.
     $edit = array(
-      'config_names[user.settings][anonymous][translation]' => 'Anonymous',
+      'translation[config_names][user.settings][anonymous]' => 'Anonymous',
     );
     $this->drupalPostForm('admin/config/people/accounts/translate/fr/edit', $edit, t('Save translation'));
 
@@ -650,6 +663,104 @@ public function testAlterInfo() {
   }
 
   /**
+   * Test text_format translation.
+   */
+  public function testTextFormatTranslation() {
+    $this->drupalLogin($this->admin_user);
+    /** @var \Drupal\Core\Config\ConfigFactoryInterface $config_factory */
+    $config_factory = $this->container->get('config.factory');
+
+    $expected = array(
+      'value' => '<p><strong>Hello World</strong></p>',
+      'format' => 'plain_text',
+    );
+    $actual = $config_factory
+      ->setOverrideState(FALSE)
+      ->get('config_translation_test.content')
+      ->get('content');
+    $this->assertEqual($expected, $actual);
+
+    $translation_base_url = 'admin/config/media/file-system/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);
+
+    $this->drupalGet($translation_page_url);
+
+    // Assert that changing the text format is not possible, even for an
+    // administrator.
+    $this->assertNoFieldByName('translation[config_names][config_translation_test.content][content][format]');
+
+    // Update translatable fields.
+    $edit = array(
+      'translation[config_names][config_translation_test.content][content][value]' => '<p><strong>Hello World</strong> - FR</p>',
+    );
+
+    // 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.
+    $expected = array(
+      'value' => '<p><strong>Hello World</strong> - FR</p>',
+      'format' => 'plain_text',
+    );
+    $this->container->get('language.config_factory_override')
+      ->setLanguage(new Language(array('id' => 'fr')));
+    $actual = $config_factory
+      ->setOverrideState(TRUE)
+      ->get('config_translation_test.content')
+      ->get('content');
+    $this->assertEqual($expected, $actual);
+
+    // Change the text format of the source configuration and verify that the
+    // text format of the translation does not change because that could lead to
+    // security vulnerabilities.
+    $config_factory
+      ->setOverrideState(FALSE)
+      ->get('config_translation_test.content')
+      ->set('content.format', 'full_html')
+      ->save();
+
+    $actual = $config_factory
+      ->setOverrideState(TRUE)
+      ->get('config_translation_test.content')
+      ->get('content');
+    // The translation should not have changed, so re-use $expected.
+    $this->assertEqual($expected, $actual);
+
+    // Because the text is now in a text format that the translator does not
+    // have access to, the translator should not be able to translate it.
+    $translation_page_url = "$translation_base_url/fr/edit";
+    $this->drupalLogin($this->translator_user);
+    $this->drupalGet($translation_page_url);
+    $this->assertDisabledTextarea('edit-translation-config-names-config-translation-testcontent-content-value');
+    $this->drupalPostForm(NULL, array(), t('Save translation'));
+    // Check that submitting the form did not update the text format of the
+    // translation.
+    $actual = $config_factory
+      ->get('config_translation_test.content')
+      ->get('content');
+    $this->assertEqual($expected, $actual);
+
+    // The administrator must explicitly change the text format.
+    $this->drupalLogin($this->admin_user);
+    $edit = array(
+      'translation[config_names][config_translation_test.content][content][format]' => 'full_html',
+    );
+    $this->drupalPostForm($translation_page_url, $edit, t('Save translation'));
+    $expected = array(
+      'value' => '<p><strong>Hello World</strong> - FR</p>',
+      'format' => 'full_html',
+    );
+    $actual = $config_factory
+      ->get('config_translation_test.content')
+      ->get('content');
+    $this->assertEqual($expected, $actual);
+  }
+
+  /**
    * Gets translation from locale storage.
    *
    * @param $config_name
@@ -717,4 +828,33 @@ protected function renderContextualLinks($ids, $current_path) {
     return $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => $current_path)));
   }
 
+  /**
+   * Asserts that a textarea with a given ID has been disabled from editing.
+   *
+   * @param string $id
+   *   The HTML ID of the textarea.
+   *
+   * @return bool
+   *   TRUE if the assertion passed; FALSE otherwise.
+   */
+  protected function assertDisabledTextarea($id) {
+    $textarea = $this->xpath('//textarea[@id=:id and contains(@disabled, "disabled")]', array(
+      ':id' => $id,
+    ));
+    $textarea = reset($textarea);
+    $passed = $this->assertTrue($textarea instanceof \SimpleXMLElement, String::format('Disabled field @id exists.', array(
+      '@id' => $id,
+    )));
+    $expected = 'This field has been disabled because you do not have sufficient permissions to edit it.';
+    $passed = $passed && $this->assertEqual((string) $textarea, $expected, String::format('Disabled textarea @id hides text in an inaccessible text format.', array(
+      '@id' => $id,
+    )));
+    // Make sure the text format select is not shown.
+    $select_id = str_replace('value', 'format--2', $id);
+    $select = $this->xpath('//select[@id=:id]', array(':id' => $select_id));
+    return $passed && $this->assertFalse($select, String::format('Field @id does not exist.', array(
+      '@id' => $id,
+    )));
+  }
+
 }
diff --git a/core/modules/config_translation/tests/modules/config_translation_test/config/install/config_translation_test.content.yml b/core/modules/config_translation/tests/modules/config_translation_test/config/install/config_translation_test.content.yml
new file mode 100644
index 0000000..5a4d4bb
--- /dev/null
+++ b/core/modules/config_translation/tests/modules/config_translation_test/config/install/config_translation_test.content.yml
@@ -0,0 +1,6 @@
+id: test
+label: 'Test'
+langcode: en
+content:
+  value: "<p><strong>Hello World</strong></p>"
+  format: plain_text
diff --git a/core/modules/config_translation/tests/modules/config_translation_test/config/schema/config_translation_test.schema.yml b/core/modules/config_translation/tests/modules/config_translation_test/config/schema/config_translation_test.schema.yml
new file mode 100644
index 0000000..757448f
--- /dev/null
+++ b/core/modules/config_translation/tests/modules/config_translation_test/config/schema/config_translation_test.schema.yml
@@ -0,0 +1,18 @@
+# Schema for the configuration files of the Configuration translation test module.
+
+config_translation_test.content:
+  type: mapping
+  label: 'Content'
+  mapping:
+    id:
+      type: string
+      label: 'Category identifier'
+    label:
+      type: label
+      label: 'Label'
+    langcode:
+      type: string
+      label: 'Default language'
+    content:
+      type: text_format
+      label: 'Content'
diff --git a/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.config_translation.yml b/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.config_translation.yml
new file mode 100644
index 0000000..070245c
--- /dev/null
+++ b/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.config_translation.yml
@@ -0,0 +1,6 @@
+# Attach to file settings for testing. The base route does not matter.
+system.file_system_settings:
+  title: 'Test config translation'
+  base_route_name: system.file_system_settings
+  names:
+    - config_translation_test.content
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
index d9308c3..8afde0f 100644
--- 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
@@ -5,4 +5,5 @@ package: Testing
 version: VERSION
 core: 8.x
 dependencies:
+  - config_translation
   - config_test
diff --git a/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.links.task.yml b/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.links.task.yml
new file mode 100644
index 0000000..92581b4
--- /dev/null
+++ b/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.links.task.yml
@@ -0,0 +1,7 @@
+# Add a default local task for the file system settings page, so that the local
+# task added by Configuration Translation becomes visible. This facilitates
+# manual testing.
+system.file_system_settings:
+  route_name: system.file_system_settings
+  title: Settings
+  base_route: system.file_system_settings
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
index 7c8cdd8..5d84070 100644
--- 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
@@ -1,3 +1,4 @@
+# Attach to performance settings for testing. The base route does not matter.
 system.performance_settings:
   title: 'Theme translation test'
   base_route_name: system.performance_settings
diff --git a/core/modules/language/src/Config/LanguageConfigFactoryOverride.php b/core/modules/language/src/Config/LanguageConfigFactoryOverride.php
index a0fbbfe..9c2e04b 100644
--- a/core/modules/language/src/Config/LanguageConfigFactoryOverride.php
+++ b/core/modules/language/src/Config/LanguageConfigFactoryOverride.php
@@ -95,7 +95,15 @@ public function loadOverrides($names) {
   public function getOverride($langcode, $name) {
     $storage = $this->getStorage($langcode);
     $data = $storage->read($name);
-    $override = new LanguageConfigOverride($name, $storage, $this->typedConfigManager);
+
+    $override = new LanguageConfigOverride(
+      $name,
+      $langcode,
+      $storage,
+      $this->typedConfigManager,
+      $this->eventDispatcher
+    );
+
     if (!empty($data)) {
       $override->initWithData($data);
     }
diff --git a/core/modules/language/src/Config/LanguageConfigOverride.php b/core/modules/language/src/Config/LanguageConfigOverride.php
index 19fb549..11a9ed7 100644
--- a/core/modules/language/src/Config/LanguageConfigOverride.php
+++ b/core/modules/language/src/Config/LanguageConfigOverride.php
@@ -7,9 +7,12 @@
 
 namespace Drupal\language\Config;
 
+use Drupal\Core\Config\ConfigEvents;
+use Drupal\Core\Config\ConfigOverrideCrudEvent;
 use Drupal\Core\Config\StorableConfigBase;
 use Drupal\Core\Config\StorageInterface;
 use Drupal\Core\Config\TypedConfigManagerInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 
 /**
  * Defines language configuration overrides.
@@ -17,20 +20,40 @@
 class LanguageConfigOverride extends StorableConfigBase {
 
   /**
+   * The language code of this language override.
+   *
+   * @var string
+   */
+  protected $langcode;
+
+  /**
+   * The event dispatcher.
+   *
+   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+   */
+  protected $eventDispatcher;
+
+  /**
    * Constructs a language override object.
    *
    * @param string $name
    *   The name of the configuration object being overridden.
+   * @param string $langcode
+   *   The language code of the language of this language override.
    * @param \Drupal\Core\Config\StorageInterface $storage
    *   A storage controller object to use for reading and writing the
    *   configuration override.
    * @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config
    *   The typed configuration manager service.
+   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
+   *   The event dispatcher.
    */
-  public function __construct($name, StorageInterface $storage, TypedConfigManagerInterface $typed_config) {
+  public function __construct($name, $langcode, StorageInterface $storage, TypedConfigManagerInterface $typed_config, EventDispatcherInterface $event_dispatcher) {
     $this->name = $name;
+    $this->langcode = $langcode;
     $this->storage = $storage;
     $this->typedConfigManager = $typed_config;
+    $this->eventDispatcher = $event_dispatcher;
   }
 
   /**
@@ -45,6 +68,7 @@ public function save() {
     }
     $this->storage->write($this->name, $this->data);
     $this->isNew = FALSE;
+    $this->eventDispatcher->dispatch(ConfigEvents::SAVE_OVERRIDE, new ConfigOverrideCrudEvent($this));
     $this->originalData = $this->data;
     return $this;
   }
@@ -56,8 +80,19 @@ public function delete() {
     $this->data = array();
     $this->storage->delete($this->name);
     $this->isNew = TRUE;
+    $this->eventDispatcher->dispatch(ConfigEvents::DELETE_OVERRIDE, new ConfigOverrideCrudEvent($this));
     $this->originalData = $this->data;
     return $this;
   }
 
+  /**
+   * Returns the language code of this language override.
+   *
+   * @return string
+   *   The language code.
+   */
+  public function getLangcode() {
+    return $this->langcode;
+  }
+
 }
diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc
index 83cd425..026fddc 100644
--- a/core/modules/locale/locale.bulk.inc
+++ b/core/modules/locale/locale.bulk.inc
@@ -5,9 +5,10 @@
  * Mass import-export and batch import functionality for Gettext .po files.
  */
 
-use Drupal\locale\Gettext;
 use Drupal\Core\Language\LanguageInterface;
 use Drupal\file\FileInterface;
+use Drupal\locale\Gettext;
+use Drupal\locale\Locale;
 
 /**
  * Prepare a batch to import all translations.
@@ -631,18 +632,26 @@ function locale_config_batch_finished($success, array $results) {
  *   Number of configuration objects retranslated.
  */
 function locale_config_update_multiple(array $names, $langcodes = array()) {
+  /** @var \Drupal\language\ConfigurableLanguageManagerInterface $language_manager */
+  $language_manager = \Drupal::languageManager();
+  $locale_config_manager = Locale::config();
+
   $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list());
   $count = 0;
   foreach ($names as $name) {
-    $wrapper = \Drupal\locale\Locale::config()->get($name);
+    $wrapper = $locale_config_manager->get($name);
     foreach ($langcodes as $langcode) {
       $translation = $wrapper->getValue() ? $wrapper->getTranslation($langcode)->getValue() : NULL;
       if ($translation) {
-        \Drupal\locale\Locale::config()->saveTranslationData($name, $langcode, $translation);
+        $locale_config_manager->saveTranslationData($name, $langcode, $translation);
         $count++;
       }
       else {
-        \Drupal\locale\Locale::config()->deleteTranslationData($name, $langcode);
+        // Do not bother deleting language overrides which do not exist in the
+        // first place.
+        if (!$language_manager->getLanguageConfigOverride($langcode, $name)->isNew()) {
+          $locale_config_manager->deleteTranslationData($name, $langcode);
+        }
       }
     }
   }
diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module
index 957dc8e..52464d2 100644
--- a/core/modules/locale/locale.module
+++ b/core/modules/locale/locale.module
@@ -320,10 +320,10 @@ function locale_modules_installed($modules) {
 }
 
 /**
- * Implements hook_modules_uninstalled().
+ * Implements hook__module_preuninstall().
  */
-function locale_modules_uninstalled($modules) {
-  $components['module'] = $modules;
+function locale_module_preuninstall($module) {
+  $components['module'] = array($module);
   locale_system_remove($components);
 }
 
diff --git a/core/modules/locale/locale.services.yml b/core/modules/locale/locale.services.yml
index e25786d..27cef2c 100644
--- a/core/modules/locale/locale.services.yml
+++ b/core/modules/locale/locale.services.yml
@@ -20,3 +20,8 @@ services:
     class: Drupal\locale\StreamWrapper\TranslationsStream
     tags:
       - { name: stream_wrapper, scheme: translations }
+  locale.config_subscriber:
+    class: Drupal\locale\LocaleConfigSubscriber
+    arguments: ['@locale.storage', '@config.factory', '@locale.config.typed']
+    tags:
+      - { name: event_subscriber }
diff --git a/core/modules/locale/src/LocaleConfigSubscriber.php b/core/modules/locale/src/LocaleConfigSubscriber.php
new file mode 100644
index 0000000..b34d556
--- /dev/null
+++ b/core/modules/locale/src/LocaleConfigSubscriber.php
@@ -0,0 +1,167 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\locale\LocaleConfigSubscriber.
+ */
+
+namespace Drupal\locale;
+
+use Drupal\Core\Config\Config;
+use Drupal\Core\Config\ConfigEvents;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\ConfigOverrideCrudEvent;
+use Drupal\Core\TypedData\TraversableTypedDataInterface;
+use Drupal\language\Config\LanguageConfigOverride;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Updates corresponding string translation when language overrides change.
+ *
+ * This reacts to the updating or deleting of configuration language overrides.
+ * It checks whether there are string translations associated with the
+ * configuration that is being saved and, if so, updates those string
+ * translations with the new configuration values and marks them as customized.
+ * That way manual updates to configuration will not be inadvertently reverted
+ * when updated translations from https://localize.drupal.org are being
+ * imported.
+ */
+class LocaleConfigSubscriber implements EventSubscriberInterface {
+
+  /**
+   * The string storage.
+   *
+   * @var \Drupal\locale\StringStorageInterface;
+   */
+  protected $stringStorage;
+
+  /**
+   * The configuration factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The typed configuration manager.
+   *
+   * @var \Drupal\locale\LocaleConfigManager
+   */
+  protected $localeConfigManager;
+
+  /**
+   * Constructs a LocaleConfigSubscriber.
+   *
+   * @param \Drupal\locale\StringStorageInterface $string_storage
+   *   The string storage.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The configuration factory.
+   * @param \Drupal\locale\LocaleConfigManager $locale_config_manager
+   *   The typed configuration manager.
+   */
+  public function __construct(StringStorageInterface $string_storage, ConfigFactoryInterface $config_factory, LocaleConfigManager $locale_config_manager) {
+    $this->stringStorage = $string_storage;
+    $this->configFactory = $config_factory;
+    $this->localeConfigManager = $locale_config_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    // Instead of deleting the actual translation strings we save empty strings
+    // when the configuration override gets deleted, so we can re-use the same
+    // function for both events.
+    $events[ConfigEvents::SAVE_OVERRIDE] = 'onOverrideUpdate';
+    $events[ConfigEvents::DELETE_OVERRIDE] = 'onOverrideUpdate';
+    return $events;
+  }
+
+
+  /**
+   * Updates the translation strings of shipped configuration.
+   *
+   * @param \Drupal\Core\Config\ConfigOverrideCrudEvent $event
+   */
+  public function onOverrideUpdate(ConfigOverrideCrudEvent $event) {
+    $translation_config = $event->getConfig();
+    $name = $translation_config->getName();
+
+    if (
+      // Only react to language overrides.
+      $translation_config instanceof LanguageConfigOverride &&
+      // Only do anything if the configuration was shipped.
+      $this->stringStorage->getLocations(array(
+        'type' => 'configuration',
+        'name' => $name,
+      ))
+    ) {
+      $source_config = $this->configFactory->get($name);
+      $schema = $this->localeConfigManager->get($name)->getTypedConfig();
+      $this->saveStrings($source_config, $translation_config, $schema);
+    }
+  }
+
+  /**
+   * Updates strings for a certain config element.
+   *
+   * @param \Drupal\Core\Config\Config $source_config
+   *   The source configuration.
+   * @param \Drupal\language\Config\LanguageConfigOverride $translation_config
+   *   The language configuration override.
+   * @param \Drupal\Core\TypedData\TraversableTypedDataInterface $schema
+   *   The respective configuration schema.
+   * @param string|null $base_key
+   *   (optional) The base key that the schema and the configuration values
+   *   belong to. This should be NULL for the top-level configuration object and
+   *   be populated consecutively when recursing into the configuration
+   *   structure.
+   */
+  protected function saveStrings(Config $source_config, LanguageConfigOverride $translation_config, TraversableTypedDataInterface $schema, $base_key = NULL) {
+    foreach ($schema as $key => $element) {
+      $element_key = implode('.', array_filter(array($base_key, $key)));
+
+      // We only care for strings here, so traverse the schema further in the
+      // case of array elements.
+      if ($element instanceof TraversableTypedDataInterface) {
+        $this->saveStrings($source_config, $translation_config, $element, $element_key);
+      }
+      else {
+        $definition = $element->getDataDefinition();
+        $source_value = $source_config->get($element_key);
+
+        // Ignore this value if it is not translatable or if no source string
+        // can be found.
+        if (
+          !empty($definition['translatable']) &&
+          $source_string = $this->stringStorage->findString(array('source' => $source_value))
+        ) {
+          // Get the translation for this original source string from locale.
+          $conditions = array(
+            'lid' => $source_string->lid,
+            'language' => $translation_config->getLangcode(),
+          );
+          $translations = $this->stringStorage->getTranslations($conditions + array('translated' => TRUE));
+          // If we got a translation, take that, otherwise create a new one.
+          $translation = reset($translations) ?: $this->stringStorage->createTranslation($conditions);
+
+          // If we have a new translation or different from what is stored in
+          // locale before, save this as an updated customize translation.
+          $value = $translation_config->get($element_key);
+          // If there is no value, save the source value as the translation.
+          // This has the same effect as deleting the string wholesale (which
+          // would be more correct) but ensures that the translation does not
+          // get re-imported when updating translations.
+          if (!isset($value)) {
+            $value = $source_value;
+          }
+          if ($translation->isNew() || $translation->getString() != $value) {
+            $translation->setString($value)
+              ->setCustomized()
+              ->save();
+          }
+        }
+      }
+    }
+  }
+
+}
diff --git a/core/modules/locale/src/Tests/LocaleConfigManagerTest.php b/core/modules/locale/src/Tests/LocaleConfigManagerTest.php
index 5e44225..f87ced5 100644
--- a/core/modules/locale/src/Tests/LocaleConfigManagerTest.php
+++ b/core/modules/locale/src/Tests/LocaleConfigManagerTest.php
@@ -9,13 +9,14 @@
 
 use Drupal\language\Entity\ConfigurableLanguage;
 use Drupal\simpletest\DrupalUnitTestBase;
+use Drupal\simpletest\KernelTestBase;
 
 /**
  * Tests that the locale config manager operates correctly.
  *
  * @group locale
  */
-class LocaleConfigManagerTest extends DrupalUnitTestBase {
+class LocaleConfigManagerTest extends KernelTestBase {
 
   /**
    * A list of modules to install for this test.
@@ -28,6 +29,7 @@ class LocaleConfigManagerTest extends DrupalUnitTestBase {
    * Tests hasTranslation().
    */
   public function testHasTranslation() {
+    $this->installSchema('locale', array('locales_location'));
     $this->installConfig(array('locale_test'));
     $locale_config_manager = \Drupal::service('locale.config.typed');
 
diff --git a/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php b/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php
index 30ab0a0..348e3a5 100644
--- a/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php
+++ b/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php
@@ -136,8 +136,10 @@ public function testConfigTranslation() {
 
     // Check the string is unique and has no translation yet.
     $translations = $this->storage->getTranslations(array('language' => $langcode, 'type' => 'configuration', 'name' => 'image.style.medium'));
+    $this->assertEqual(count($translations), 1);
     $translation = reset($translations);
-    $this->assertTrue(count($translations) == 1 && $translation->source == $string->source && empty($translation->translation), 'Got only one string for image configuration and has no translation.');
+    $this->assertEqual($translation->source, $string->source);
+    $this->assertTrue(empty($translation->translation));
 
     // Translate using the UI so configuration is refreshed.
     $image_style_label = $this->randomMachineName(20);
