summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlex Pott2014-11-24 09:46:43 (GMT)
committerAlex Pott2014-11-24 09:46:43 (GMT)
commit1a8858a0a221b8b8b4ed53786540ab0ea7254096 (patch)
tree811859863f204db456a67c39a57abf53447e3ade
parent9f6975a85ea3c108cc56e6bf14dbb571922a8f90 (diff)
Issue #2144413 by tstoeckler, YesCT, robertdbailey, webflo, Schnitzel, Wim Leers, kfritsche, Jose Reyero, Gábor Hojtsy, prodosh: Config translation does not support text elements with a format
-rw-r--r--core/config/schema/core.data_types.schema.yml23
-rw-r--r--core/lib/Drupal/Core/Config/ConfigEvents.php2
-rw-r--r--core/lib/Drupal/Core/Config/TypedConfigManagerInterface.php2
-rw-r--r--core/modules/config_translation/config_translation.module16
-rw-r--r--core/modules/config_translation/src/Form/ConfigTranslationFormBase.php220
-rw-r--r--core/modules/config_translation/src/FormElement/DateFormat.php15
-rw-r--r--core/modules/config_translation/src/FormElement/ElementInterface.php59
-rw-r--r--core/modules/config_translation/src/FormElement/FormElementBase.php188
-rw-r--r--core/modules/config_translation/src/FormElement/ListElement.php137
-rw-r--r--core/modules/config_translation/src/FormElement/TextFormat.php45
-rw-r--r--core/modules/config_translation/src/FormElement/Textarea.php16
-rw-r--r--core/modules/config_translation/src/FormElement/Textfield.php12
-rw-r--r--core/modules/config_translation/src/Tests/ConfigTranslationFormTest.php2
-rw-r--r--core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php200
-rw-r--r--core/modules/config_translation/tests/modules/config_translation_test/config/install/config_translation_test.content.yml6
-rw-r--r--core/modules/config_translation/tests/modules/config_translation_test/config/schema/config_translation_test.schema.yml18
-rw-r--r--core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.config_translation.yml6
-rw-r--r--core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.info.yml1
-rw-r--r--core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.links.task.yml7
-rw-r--r--core/modules/config_translation/tests/themes/config_translation_test_theme/config_translation_test_theme.config_translation.yml1
-rw-r--r--core/modules/language/src/Config/LanguageConfigCollectionNameTrait.php53
-rw-r--r--core/modules/language/src/Config/LanguageConfigFactoryOverride.php48
-rw-r--r--core/modules/language/src/Config/LanguageConfigOverride.php27
-rw-r--r--core/modules/language/src/Config/LanguageConfigOverrideCrudEvent.php46
-rw-r--r--core/modules/language/src/Config/LanguageConfigOverrideEvents.php33
-rw-r--r--core/modules/locale/locale.bulk.inc17
-rw-r--r--core/modules/locale/locale.module6
-rw-r--r--core/modules/locale/locale.services.yml5
-rw-r--r--core/modules/locale/src/LocaleConfigManager.php25
-rw-r--r--core/modules/locale/src/LocaleConfigSubscriber.php275
-rw-r--r--core/modules/locale/src/Tests/LocaleConfigManagerTest.php5
-rw-r--r--core/modules/locale/src/Tests/LocaleConfigSubscriberTest.php397
-rw-r--r--core/modules/locale/src/Tests/LocaleConfigTranslationTest.php4
-rw-r--r--core/modules/locale/tests/modules/locale_test/config/schema/locale_test.schema.yml4
34 files changed, 1614 insertions, 307 deletions
diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index b28eb75..57aa1a1 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -737,3 +737,26 @@ field.float.value:
value:
type: float
label: 'Value'
+
+# Text with a text format.
+text_format:
+ type: mapping
+ label: 'Text with text format'
+ # We declare the entire mapping of text and text format 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'
+ # Mark the actual text as translatable (in addition to the entire mapping
+ # being marked as translatable) so that shipped configuration with
+ # formatted text can participate in the string translation system.
+ translatable: true
+ format:
+ type: string
+ label: 'Text format'
+ # The text format should not be translated as part of the string
+ # translation system, so this is not marked as translatable.
diff --git a/core/lib/Drupal/Core/Config/ConfigEvents.php b/core/lib/Drupal/Core/Config/ConfigEvents.php
index 7fa22a5..7fc0750 100644
--- a/core/lib/Drupal/Core/Config/ConfigEvents.php
+++ b/core/lib/Drupal/Core/Config/ConfigEvents.php
@@ -9,6 +9,8 @@ namespace Drupal\Core\Config;
/**
* Defines events for the configuration system.
+ *
+ * @see \Drupal\Core\Config\ConfigCrudEvent
*/
final class ConfigEvents {
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 @@ Interface TypedConfigManagerInterface extends PluginManagerInterface, CachedDisc
* @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..336a405 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($map[$type]) && !isset($definition['form_element_class'])) {
+ $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 5e05293..9385120 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 @@ abstract class ConfigTranslationFormBase extends FormBase implements BaseFormIdI
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 @@ abstract class ConfigTranslationFormBase extends FormBase implements BaseFormIdI
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 @@ abstract class ConfigTranslationFormBase extends FormBase implements BaseFormIdI
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 @@ abstract class ConfigTranslationFormBase extends FormBase implements BaseFormIdI
$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 @@ abstract class ConfigTranslationFormBase extends FormBase implements BaseFormIdI
* {@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 @@ abstract class ConfigTranslationFormBase extends FormBase implements BaseFormIdI
$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 @@ abstract class ConfigTranslationFormBase extends FormBase implements BaseFormIdI
}
/**
- * Formats configuration schema as a form tree.
+ * Creates a 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 is used for Mapping and
+ // Sequence 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->getName(),
- )
- ),
- '#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 c3478e6..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\AjaxResponse;
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->getName() . ')</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..286eeba 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 @@ use Drupal\Core\TypedData\DataDefinitionInterface;
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);
+
+ /**
+ * Builds a render array containg the source and translation form elements.
+ *
+ * @param \Drupal\Core\Language\LanguageInterface $source_language
+ * The 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|null $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 consisting of the source and translation elements 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, array $parents, $base_key = NULL);
+ /**
+ * Sets configuration based on a nested form value array.
+ *
+ * If the configuration values are the same as the source configuration, the
+ * override should be removed from the translation configuration.
+ *
+ * @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.
+ */
+ 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..7db01e1
--- /dev/null
+++ b/core/modules/config_translation/src/FormElement/FormElementBase.php
@@ -0,0 +1,188 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\config_translation\FormElement\FormElementBase.
+ */
+
+namespace Drupal\config_translation\FormElement;
+
+use Drupal\Core\Config\Config;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+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, array $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 the configuration values, if they are different from the source
+ // values in the base configuration. Otherwise remove the 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..8bd8a52
--- /dev/null
+++ b/core/modules/config_translation/src/FormElement/ListElement.php
@@ -0,0 +1,137 @@
+<?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, array $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 the 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) {
+ $element_key = implode('.', array_filter(array($base_key, $key)));
+ if ($form_element = ConfigTranslationFormBase::createFormElement($element)) {
+ // Traverse into the next level of the configuration.
+ $value = isset($config_values[$key]) ? $config_values[$key] : NULL;
+ $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 definition of the schema element.
+ * @param array $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, array $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..c06c449
--- /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\FormElementBase::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 804301c..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->getName() . ')</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 c28ca8d..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->getName() . ')</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 3bab5a6..d15d8f3 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', 'contact_test', 'config_translation', 'config_translation_test', 'views', 'views_ui', 'contextual');
+ public static $modules = array('node', 'contact', 'contact_test', 'config_translation', 'config_translation_test', 'views', 'views_ui', 'contextual', 'filter', 'filter_test');
/**
* Languages to enable.
@@ -62,6 +63,14 @@ class ConfigTranslationUiTest extends WebTestBase {
$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(
@@ -69,6 +78,10 @@ class ConfigTranslationUiTest extends WebTestBase {
'administer site configuration',
'link to any page',
'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',
@@ -124,8 +137,8 @@ class ConfigTranslationUiTest extends WebTestBase {
// 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'));
@@ -138,8 +151,8 @@ class ConfigTranslationUiTest extends WebTestBase {
// 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');
@@ -167,8 +180,8 @@ class ConfigTranslationUiTest extends WebTestBase {
// 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'));
@@ -190,8 +203,8 @@ class ConfigTranslationUiTest extends WebTestBase {
$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')));
@@ -205,8 +218,8 @@ class ConfigTranslationUiTest extends WebTestBase {
$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');
@@ -275,8 +288,8 @@ class ConfigTranslationUiTest extends WebTestBase {
// 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.
@@ -314,7 +327,7 @@ class ConfigTranslationUiTest extends WebTestBase {
$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);
}
}
@@ -404,8 +417,8 @@ class ConfigTranslationUiTest extends WebTestBase {
// 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.
@@ -445,9 +458,9 @@ class ConfigTranslationUiTest extends WebTestBase {
// 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'));
@@ -456,7 +469,7 @@ class ConfigTranslationUiTest extends WebTestBase {
$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.
@@ -543,10 +556,10 @@ class ConfigTranslationUiTest extends WebTestBase {
// 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')));
@@ -558,10 +571,10 @@ class ConfigTranslationUiTest extends WebTestBase {
// 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");
}
/**
@@ -592,7 +605,7 @@ class ConfigTranslationUiTest extends WebTestBase {
// 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'));
@@ -603,7 +616,7 @@ class ConfigTranslationUiTest extends WebTestBase {
// 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'));
@@ -661,6 +674,104 @@ class ConfigTranslationUiTest extends WebTestBase {
}
/**
+ * 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
@@ -728,4 +839,33 @@ class ConfigTranslationUiTest extends WebTestBase {
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/LanguageConfigCollectionNameTrait.php b/core/modules/language/src/Config/LanguageConfigCollectionNameTrait.php
new file mode 100644
index 0000000..6f7b1ad
--- /dev/null
+++ b/core/modules/language/src/Config/LanguageConfigCollectionNameTrait.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\language\Config\LanguageConfigCollectionNameTrait.
+ */
+
+namespace Drupal\language\Config;
+
+use Drupal\Component\Utility\String;
+
+/**
+ * Provides a common trait for working with language override collection names.
+ */
+trait LanguageConfigCollectionNameTrait {
+
+ /**
+ * Creates a configuration collection name based on a language code.
+ *
+ * @param string $langcode
+ * The language code.
+ *
+ * @return string
+ * The configuration collection name for a language code.
+ */
+ protected function createConfigCollectionName($langcode) {
+ return 'language.' . $langcode;
+ }
+
+ /**
+ * Converts a configuration collection name to a language code.
+ *
+ * @param string $collection
+ * The configuration collection name.
+ *
+ * @return string
+ * The language code of the collection.
+ *
+ * @throws \InvalidArgumentException
+ * Exception thrown if the provided collection name is not in the format
+ * "language.LANGCODE".
+ *
+ * @see self::createConfigCollectionName()
+ */
+ protected function getLangcodeFromCollectionName($collection) {
+ preg_match('/^language\.(.*)$/', $collection, $matches);
+ if (!isset($matches[1])) {
+ throw new \InvalidArgumentException(String::format('!collection is not a valid language override collection', array('!collection' => $collection)));
+ }
+ return $matches[1];
+ }
+
+}
diff --git a/core/modules/language/src/Config/LanguageConfigFactoryOverride.php b/core/modules/language/src/Config/LanguageConfigFactoryOverride.php
index a0fbbfe..3d0a67b 100644
--- a/core/modules/language/src/Config/LanguageConfigFactoryOverride.php
+++ b/core/modules/language/src/Config/LanguageConfigFactoryOverride.php
@@ -7,7 +7,6 @@
namespace Drupal\language\Config;
-use Drupal\Component\Utility\String;
use Drupal\Core\Config\ConfigCollectionInfo;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigFactoryOverrideBase;
@@ -24,6 +23,8 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
*/
class LanguageConfigFactoryOverride extends ConfigFactoryOverrideBase implements LanguageConfigFactoryOverrideInterface, EventSubscriberInterface {
+ use LanguageConfigCollectionNameTrait;
+
/**
* The configuration storage.
*
@@ -95,7 +96,14 @@ class LanguageConfigFactoryOverride extends ConfigFactoryOverrideBase implements
public function getOverride($langcode, $name) {
$storage = $this->getStorage($langcode);
$data = $storage->read($name);
- $override = new LanguageConfigOverride($name, $storage, $this->typedConfigManager);
+
+ $override = new LanguageConfigOverride(
+ $name,
+ $storage,
+ $this->typedConfigManager,
+ $this->eventDispatcher
+ );
+
if (!empty($data)) {
$override->initWithData($data);
}
@@ -160,42 +168,6 @@ class LanguageConfigFactoryOverride extends ConfigFactoryOverrideBase implements
}
/**
- * Creates a configuration collection name based on a langcode.
- *
- * @param string $langcode
- * The langcode.
- *
- * @return string
- * The configuration collection name for a langcode.
- */
- protected function createConfigCollectionName($langcode) {
- return 'language.' . $langcode;
- }
-
- /**
- * Converts a configuration collection name to a langcode.
- *
- * @param string $collection
- * The configuration collection name.
- *
- * @return string
- * The langcode of the collection.
- *
- * @throws \InvalidArgumentException
- * Exception thrown if the provided collection name is not in the format
- * "language.LANGCODE".
- *
- * @see self::createConfigCollectionName()
- */
- protected function getLangcodeFromCollectionName($collection) {
- preg_match('/^language\.(.*)$/', $collection, $matches);
- if (!isset($matches[1])) {
- throw new \InvalidArgumentException(String::format('!collection is not a valid language override collection', array('!collection' => $collection)));
- }
- return $matches[1];
- }
-
- /**
* {@inheritdoc}
*/
public function addCollections(ConfigCollectionInfo $collection_info) {
diff --git a/core/modules/language/src/Config/LanguageConfigOverride.php b/core/modules/language/src/Config/LanguageConfigOverride.php
index 19fb549..d64dda7 100644
--- a/core/modules/language/src/Config/LanguageConfigOverride.php
+++ b/core/modules/language/src/Config/LanguageConfigOverride.php
@@ -10,12 +10,22 @@ namespace Drupal\language\Config;
use Drupal\Core\Config\StorableConfigBase;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Config\TypedConfigManagerInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Defines language configuration overrides.
*/
class LanguageConfigOverride extends StorableConfigBase {
+ use LanguageConfigCollectionNameTrait;
+
+ /**
+ * The event dispatcher.
+ *
+ * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+ */
+ protected $eventDispatcher;
+
/**
* Constructs a language override object.
*
@@ -26,11 +36,14 @@ class LanguageConfigOverride extends StorableConfigBase {
* 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, StorageInterface $storage, TypedConfigManagerInterface $typed_config, EventDispatcherInterface $event_dispatcher) {
$this->name = $name;
$this->storage = $storage;
$this->typedConfigManager = $typed_config;
+ $this->eventDispatcher = $event_dispatcher;
}
/**
@@ -45,6 +58,7 @@ class LanguageConfigOverride extends StorableConfigBase {
}
$this->storage->write($this->name, $this->data);
$this->isNew = FALSE;
+ $this->eventDispatcher->dispatch(LanguageConfigOverrideEvents::SAVE_OVERRIDE, new LanguageConfigOverrideCrudEvent($this));
$this->originalData = $this->data;
return $this;
}
@@ -56,8 +70,19 @@ class LanguageConfigOverride extends StorableConfigBase {
$this->data = array();
$this->storage->delete($this->name);
$this->isNew = TRUE;
+ $this->eventDispatcher->dispatch(LanguageConfigOverrideEvents::DELETE_OVERRIDE, new LanguageConfigOverrideCrudEvent($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->getLangcodeFromCollectionName($this->getStorage()->getCollectionName());
+ }
+
}
diff --git a/core/modules/language/src/Config/LanguageConfigOverrideCrudEvent.php b/core/modules/language/src/Config/LanguageConfigOverrideCrudEvent.php
new file mode 100644
index 0000000..3b91d07
--- /dev/null
+++ b/core/modules/language/src/Config/LanguageConfigOverrideCrudEvent.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\language\Config\LanguageConfigOverrideCrudEvent.
+ */
+
+namespace Drupal\language\Config;
+
+use Symfony\Component\EventDispatcher\Event;
+
+/**
+ * Provides a language override event for event listeners.
+ *
+ * @see \Drupal\Core\Config\ConfigCrudEvent
+ */
+class LanguageConfigOverrideCrudEvent extends Event {
+
+ /**
+ * Configuration object.
+ *
+ * @var \Drupal\language\Config\LanguageConfigOverride
+ */
+ protected $override;
+
+ /**
+ * Constructs a configuration event object.
+ *
+ * @param \Drupal\language\Config\LanguageConfigOverride $override
+ * Configuration object.
+ */
+ public function __construct(LanguageConfigOverride $override) {
+ $this->override = $override;
+ }
+
+ /**
+ * Gets configuration object.
+ *
+ * @return \Drupal\language\Config\LanguageConfigOverride
+ * The configuration object that caused the event to fire.
+ */
+ public function getLanguageConfigOverride() {
+ return $this->override;
+ }
+
+}
diff --git a/core/modules/language/src/Config/LanguageConfigOverrideEvents.php b/core/modules/language/src/Config/LanguageConfigOverrideEvents.php
new file mode 100644
index 0000000..7770f59
--- /dev/null
+++ b/core/modules/language/src/Config/LanguageConfigOverrideEvents.php
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\language\Config\LanguageConfigOverrideEvents.
+ */
+
+namespace Drupal\language\Config;
+
+/**
+ * Defines events for language configuration overrides.
+ *
+ * @see \Drupal\Core\Config\ConfigCrudEvent
+ */
+final class LanguageConfigOverrideEvents {
+
+ /**
+ * The name of the event fired when saving the configuration override.
+ *
+ * @see \Drupal\language\Config\LanguageConfigOverrideCrudEvent
+ * @see \Drupal\language\Config\LanguageConfigOverride::save()
+ */
+ const SAVE_OVERRIDE = 'language.save_override';
+
+ /**
+ * The name of the event fired when deleting the configuration override.
+ *
+ * @see \Drupal\language\Config\LanguageConfigOverrideCrudEvent
+ * @see \Drupal\language\Config\LanguageConfigOverride::delete()
+ */
+ const DELETE_OVERRIDE = 'language.delete_override';
+
+}
diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc
index 991a89c..9206225 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.
@@ -639,18 +640,26 @@ function locale_config_batch_finished($success, array $results) {
* Number of configuration objects retranslated.
*/
function locale_config_update_multiple(array $names, array $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 623a658..05ea103 100644
--- a/core/modules/locale/locale.module
+++ b/core/modules/locale/locale.module
@@ -321,10 +321,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/LocaleConfigManager.php b/core/modules/locale/src/LocaleConfigManager.php
index e2f4452..7bc846e 100644
--- a/core/modules/locale/src/LocaleConfigManager.php
+++ b/core/modules/locale/src/LocaleConfigManager.php
@@ -66,6 +66,13 @@ class LocaleConfigManager {
protected $typedConfigManager;
/**
+ * Whether or not configuration translations are currently being updated.
+ *
+ * @var bool
+ */
+ protected $isUpdating = FALSE;
+
+ /**
* Creates a new typed configuration manager.
*
* @param \Drupal\Core\Config\StorageInterface $config_storage
@@ -156,7 +163,9 @@ class LocaleConfigManager {
* Configuration data to be saved, that will be only the translated values.
*/
public function saveTranslationData($name, $langcode, array $data) {
+ $this->isUpdating = TRUE;
$this->languageManager->getLanguageConfigOverride($langcode, $name)->setData($data)->save();
+ $this->isUpdating = FALSE;
}
/**
@@ -168,7 +177,9 @@ class LocaleConfigManager {
* Language code.
*/
public function deleteTranslationData($name, $langcode) {
+ $this->isUpdating = TRUE;
$this->languageManager->getLanguageConfigOverride($langcode, $name)->delete();
+ $this->isUpdating = FALSE;
}
/**
@@ -206,6 +217,7 @@ class LocaleConfigManager {
* Array of language codes.
*/
public function deleteComponentTranslations(array $components, array $langcodes) {
+ $this->isUpdating = TRUE;
$names = $this->getComponentNames($components);
if ($names && $langcodes) {
foreach ($names as $name) {
@@ -214,6 +226,7 @@ class LocaleConfigManager {
}
}
}
+ $this->isUpdating = FALSE;
}
/**
@@ -241,10 +254,12 @@ class LocaleConfigManager {
* Language code to delete.
*/
public function deleteLanguageTranslations($langcode) {
+ $this->isUpdating = TRUE;
$storage = $this->languageManager->getLanguageConfigOverrideStorage($langcode);
foreach ($storage->listAll() as $name) {
$this->languageManager->getLanguageConfigOverride($langcode, $name)->delete();
}
+ $this->isUpdating = FALSE;
}
/**
@@ -330,4 +345,14 @@ class LocaleConfigManager {
return !$translation->isNew();
}
+ /**
+ * Indicates whether configuration translations are currently being updated.
+ *
+ * @return bool
+ * Whether or not configuration translations are currently being updated.
+ */
+ public function isUpdatingConfigTranslations() {
+ return $this->isUpdating;
+ }
+
}
diff --git a/core/modules/locale/src/LocaleConfigSubscriber.php b/core/modules/locale/src/LocaleConfigSubscriber.php
new file mode 100644
index 0000000..84ad835
--- /dev/null
+++ b/core/modules/locale/src/LocaleConfigSubscriber.php
@@ -0,0 +1,275 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\locale\LocaleConfigSubscriber.
+ */
+
+namespace Drupal\locale;
+
+use Drupal\Core\Config\Config;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\TypedData\TraversableTypedDataInterface;
+use Drupal\language\Config\LanguageConfigOverride;
+use Drupal\language\Config\LanguageConfigOverrideCrudEvent;
+use Drupal\language\Config\LanguageConfigOverrideEvents;
+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() {
+ $events[LanguageConfigOverrideEvents::SAVE_OVERRIDE] = 'onSave';
+ $events[LanguageConfigOverrideEvents::DELETE_OVERRIDE] = 'onDelete';
+ return $events;
+ }
+
+
+ /**
+ * Updates the translation strings when shipped configuration is saved.
+ *
+ * @param \Drupal\language\Config\LanguageConfigOverrideCrudEvent $event
+ * The language configuration event.
+ */
+ public function onSave(LanguageConfigOverrideCrudEvent $event) {
+ // Do not mark strings as customized when community translations are being
+ // imported.
+ if ($this->localeConfigManager->isUpdatingConfigTranslations()) {
+ $callable = [$this, 'saveTranslation'];
+ }
+ else {
+ $callable = [$this, 'saveCustomizedTranslation'];
+ }
+
+ $this->updateTranslationStrings($event, $callable);
+ }
+
+ /**
+ * Updates the translation strings when shipped configuration is deleted.
+ *
+ * @param \Drupal\language\Config\LanguageConfigOverrideCrudEvent $event
+ * The language configuration event.
+ */
+ public function onDelete(LanguageConfigOverrideCrudEvent $event) {
+ if ($this->localeConfigManager->isUpdatingConfigTranslations()) {
+ $callable = [$this, 'deleteTranslation'];
+ }
+ else {
+ // Do not delete the string, but save a customized translation with the
+ // source value so that the deletion will not be reverted by importing
+ // community translations.
+ // @see \Drupal\locale\LocaleConfigSubscriber::saveCustomizedTranslation()
+ $callable = [$this, 'saveCustomizedTranslation'];
+ }
+
+ $this->updateTranslationStrings($event, $callable);
+ }
+
+ /**
+ * Updates the translation strings of shipped configuration.
+ *
+ * @param \Drupal\language\Config\LanguageConfigOverrideCrudEvent $event
+ * The language configuration event.
+ * @param $callable
+ * A callable to apply to each translatable string of the configuration.
+ */
+ protected function updateTranslationStrings(LanguageConfigOverrideCrudEvent $event, $callable) {
+ $translation_config = $event->getLanguageConfigOverride();
+ $name = $translation_config->getName();
+
+ // Only do anything if the configuration was shipped.
+ if ($this->stringStorage->getLocations(['type' => 'configuration', 'name' => $name])) {
+ $override_state = $this->configFactory->getOverrideState();
+ $this->configFactory->setOverrideState(FALSE);
+
+ $source_config = $this->configFactory->get($name);
+ $schema = $this->localeConfigManager->get($name)->getTypedConfig();
+
+ $this->traverseSchema($schema, $source_config, $translation_config, $callable);
+
+ $this->configFactory->setOverrideState($override_state);
+ }
+ }
+
+ /**
+ * Traverses configuration schema and applies a callback to each leaf element.
+ *
+ * It skips leaf elements that are not translatable.
+ *
+ * @param \Drupal\Core\TypedData\TraversableTypedDataInterface $schema
+ * The respective configuration schema.
+ * @param callable $callable
+ * The callable to apply to each leaf element. The callable will be called
+ * with the leaf element and the element key as arguments.
+ * @param string|null $base_key
+ * (optional) The base key that the schema belongs to. This should be NULL
+ * for the top-level schema and be populated consecutively when recursing
+ * into the schema structure.
+ */
+ protected function traverseSchema(TraversableTypedDataInterface $schema, Config $source_config, LanguageConfigOverride $translation_config, $callable, $base_key = NULL) {
+ foreach ($schema as $key => $element) {
+ $element_key = implode('.', array_filter([$base_key, $key]));
+
+ // We only care for strings here, so traverse the schema further in the
+ // case of traversable elements.
+ if ($element instanceof TraversableTypedDataInterface) {
+ $this->traverseSchema($element, $source_config, $translation_config, $callable, $element_key);
+ }
+ // Skip elements which are not translatable.
+ elseif (!empty($element->getDataDefinition()['translatable'])) {
+ $callable(
+ $source_config->get($element_key),
+ $translation_config->getLangcode(),
+ $translation_config->get($element_key)
+ );
+ }
+ }
+ }
+
+ /**
+ * Saves a translation string.
+ *
+ * @param string $source_value
+ * The source string value.
+ * @param string $langcode
+ * The language code of the translation.
+ * @param string|null $translation_value
+ * (optional) The translation string value. If omitted, no translation will
+ * be saved.
+ */
+ protected function saveTranslation($source_value, $langcode, $translation_value = NULL) {
+ if ($translation_value && ($translation = $this->getTranslation($source_value, $langcode, TRUE))) {
+ if ($translation->isNew() || $translation->getString() != $translation_value) {
+ $translation
+ ->setString($translation_value)
+ ->save();
+ }
+ }
+ }
+
+ /**
+ * Saves a translation string and marks it as customized.
+ *
+ * @param string $source_value
+ * The source string value.
+ * @param string $langcode
+ * The language code of the translation.
+ * @param string|null $translation_value
+ * (optional) The translation string value. If omitted, a customized string
+ * with the source value will be saved.
+ *
+ * @see \Drupal\locale\LocaleConfigSubscriber::onDelete()
+ */
+ protected function saveCustomizedTranslation($source_value, $langcode, $translation_value = NULL) {
+ if ($translation = $this->getTranslation($source_value, $langcode, TRUE)) {
+ if (!isset($translation_value)) {
+ $translation_value = $source_value;
+ }
+ if ($translation->isNew() || $translation->getString() != $translation_value) {
+ $translation
+ ->setString($translation_value)
+ ->setCustomized(TRUE)
+ ->save();
+ }
+ }
+ }
+
+ /**
+ * Deletes a translation string, if it exists.
+ *
+ * @param string $source_value
+ * The source string value.
+ * @param string $langcode
+ * The language code of the translation.
+ *
+ * @see \Drupal\locale\LocaleConfigSubscriber::onDelete()
+ */
+ protected function deleteTranslation($source_value, $langcode) {
+ if ($translation = $this->getTranslation($source_value, $langcode, FALSE)) {
+ $translation->delete();
+ }
+ }
+
+ /**
+ * Gets a translation string.
+ *
+ * @param string $source_value
+ * The source string value.
+ * @param string $langcode
+ * The language code of the translation.
+ * @param bool $create_fallback
+ * (optional) By default if a source string could be found and no
+ * translation in the given language exists yet, a translation object is
+ * created. This can be circumvented by passing FALSE.
+ *
+ * @return \Drupal\locale\TranslationString|null
+ * The translation string if one was found or created.
+ */
+ protected function getTranslation($source_value, $langcode, $create_fallback = TRUE) {
+ // There is no point in creating a translation without a source.
+ if ($source_string = $this->stringStorage->findString(['source' => $source_value])) {
+ // Get the translation for this original source string from locale.
+ $conditions = [
+ 'lid' => $source_string->lid,
+ 'language' => $langcode,
+ ];
+ $translations = $this->stringStorage->getTranslations($conditions + ['translated' => TRUE]);
+ if ($translations) {
+ return reset($translations);
+ }
+ elseif ($create_fallback) {
+ return $this->stringStorage->createTranslation($conditions);
+ }
+ }
+ }
+
+}
diff --git a/core/modules/locale/src/Tests/LocaleConfigManagerTest.php b/core/modules/locale/src/Tests/LocaleConfigManagerTest.php
index 5e44225..4cc9fb2 100644
--- a/core/modules/locale/src/Tests/LocaleConfigManagerTest.php
+++ b/core/modules/locale/src/Tests/LocaleConfigManagerTest.php
@@ -8,14 +8,14 @@
namespace Drupal\locale\Tests;
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 +28,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/LocaleConfigSubscriberTest.php b/core/modules/locale/src/Tests/LocaleConfigSubscriberTest.php
new file mode 100644
index 0000000..2983af9
--- /dev/null
+++ b/core/modules/locale/src/Tests/LocaleConfigSubscriberTest.php
@@ -0,0 +1,397 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\locale\Tests\LocaleConfigSubscriberTest.
+ */
+
+namespace Drupal\locale\Tests;
+
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\locale\StringInterface;
+use Drupal\locale\TranslationString;
+use Drupal\simpletest\KernelTestBase;
+
+/**
+ * Tests that shipped configuration translations are updated correctly.
+ *
+ * @group locale
+ */
+class LocaleConfigSubscriberTest extends KernelTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public static $modules = ['language', 'locale', 'locale_test'];
+
+ /**
+ * The configurable language manager used in this test.
+ *
+ * @var \Drupal\language\ConfigurableLanguageManagerInterface
+ */
+ protected $languageManager;
+
+ /**
+ * The configuration factory used in this test.
+ *
+ * @var \Drupal\Core\Config\ConfigFactoryInterface
+ */
+ protected $configFactory;
+
+ /**
+ * The string storage used in this test.
+ *
+ * @var \Drupal\locale\StringStorageInterface;
+ */
+ protected $stringStorage;
+
+ /**
+ * The locale configuration manager used in this test.
+ *
+ * @var \Drupal\locale\LocaleConfigManager
+ */
+ protected $localeConfigManager;
+
+ /**
+ * The language code used in this test.
+ *
+ * @var string
+ */
+ protected $langcode = 'de';
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+
+ $this->languageManager = $this->container->get('language_manager');
+ $this->configFactory = $this->container->get('config.factory');
+ $this->stringStorage = $this->container->get('locale.storage');
+ $this->localeConfigManager = $this->container->get('locale.config.typed');
+
+ $this->installSchema('locale', ['locales_source', 'locales_target', 'locales_location']);
+
+ $this->installConfig(['locale_test']);
+ ConfigurableLanguage::createFromLangcode($this->langcode)->save();
+ }
+
+ /**
+ * Tests creating translations of shipped configuration.
+ */
+ public function testCreateTranslation() {
+ $config_name = 'locale_test.no_translation';
+
+ $this->setUpNoTranslation($config_name, 'test', 'Test');
+ $this->saveLanguageOverride($config_name, 'test', 'Test (German)');
+ $this->assertTranslation($config_name, 'Test (German)');
+ }
+
+ /**
+ * Tests importing community translations of shipped configuration.
+ */
+ public function testLocaleCreateTranslation() {
+ $config_name = 'locale_test.no_translation';
+
+ $this->setUpNoTranslation($config_name, 'test', 'Test');
+ $this->saveLocaleTranslationData($config_name, 'test', 'Test (German)');
+ $this->assertTranslation($config_name, 'Test (German)', FALSE);
+ }
+
+ /**
+ * Tests updating translations of shipped configuration.
+ */
+ public function testUpdateTranslation() {
+ $config_name = 'locale_test.translation';
+
+ $this->setUpTranslation($config_name, 'test', 'English test', 'German test');
+ $this->saveLanguageOverride($config_name, 'test', 'Updated German test');
+ $this->assertTranslation($config_name, 'Updated German test');
+ }
+
+ /**
+ * Tests updating community translations of shipped configuration.
+ */
+ public function testLocaleUpdateTranslation() {
+ $config_name = 'locale_test.translation';
+
+ $this->setUpTranslation($config_name, 'test', 'English test', 'German test');
+ $this->saveLocaleTranslationData($config_name, 'test', 'Updated German test');
+ $this->assertTranslation($config_name, 'Updated German test', FALSE);
+ }
+
+ /**
+ * Tests deleting translations of shipped configuration.
+ */
+ public function testDeleteTranslation() {
+ $config_name = 'locale_test.translation';
+
+ $this->setUpTranslation($config_name, 'test', 'English test', 'German test');
+ $this->deleteLanguageOverride($config_name, 'test', 'English test');
+ // Instead of deleting the translation, we need to keep a translation with
+ // the source value and mark it as customized to prevent the deletion being
+ // reverted by importing community translations.
+ $this->assertTranslation($config_name, 'English test');
+ }
+
+ /**
+ * Tests deleting community translations of shipped configuration.
+ */
+ public function testLocaleDeleteTranslation() {
+ $config_name = 'locale_test.translation';
+
+ $this->setUpTranslation($config_name, 'test', 'English test', 'German test');
+ $this->deleteLocaleTranslationData($config_name, 'test', 'English test');
+ $this->assertNoTranslation($config_name, 'English test', FALSE);
+ }
+
+ /**
+ * Sets up a configuration string without a translation.
+ *
+ * The actual configuration is already available by installing locale_test
+ * module, as it is done in LocaleConfigSubscriberTest::setUp(). This sets up
+ * the necessary source string and verifies that everything is as expected to
+ * avoid false positives.
+ *
+ * @param string $config_name
+ * The configuration name.
+ * @param string $key
+ * The configuration key.
+ * @param string $source
+ * The source string.
+ */
+ protected function setUpNoTranslation($config_name, $key, $source) {
+ // Add a source string with the configuration name as a location. This gets
+ // called from locale_config_update_multiple() normally.
+ $this->localeConfigManager->translateString($config_name, $this->langcode, $source, '');
+ $this->languageManager
+ ->setConfigOverrideLanguage(ConfigurableLanguage::load($this->langcode));
+
+ $this->assertConfigValue($config_name, $key, $source);
+ $this->assertNoTranslation($config_name);
+ }
+
+
+ /**
+ * Sets up a configuration string with a translation.
+ *
+ * The actual configuration is already available by installing locale_test
+ * module, as it is done in LocaleConfigSubscriberTest::setUp(). This sets up
+ * the necessary source and translation strings and verifies that everything
+ * is as expected to avoid false positives.
+ *
+ * @param string $config_name
+ * The configuration name.
+ * @param string $key
+ * The configuration key.
+ * @param string $source
+ * The source string.
+ * @param string $translation
+ * The translation string.
+ */
+ protected function setUpTranslation($config_name, $key, $source, $translation) {
+ // Create source and translation strings for the configuration value and add
+ // the configuration name as a location. This would be performed by
+ // locale_translate_batch_import() and locale_config_update_multiple()
+ // normally.
+ $source_object = $this->stringStorage->createString([
+ 'source' => $source,
+ 'context' => '',
+ ])->save();
+ $this->stringStorage->createTranslation([
+ 'lid' => $source_object->getId(),
+ 'language' => $this->langcode,
+ 'translation' => $translation,
+ ])->save();
+ $this->localeConfigManager->translateString($config_name, $this->langcode, $source, '');
+ $this->languageManager
+ ->setConfigOverrideLanguage(ConfigurableLanguage::load($this->langcode));
+
+ $this->assertConfigValue($config_name, $key, $translation);
+ $this->assertTranslation($config_name, $translation, FALSE);
+ }
+
+ /**
+ * Saves a language override.
+ *
+ * This will invoke LocaleConfigSubscriber through the event dispatcher. To
+ * make sure the configuration was persisted correctly, the configuration
+ * value is checked. Because LocaleConfigSubscriber temporarily disables the
+ * override state of the configuration factory we check that the correct value
+ * is restored afterwards.
+ *
+ * @param string $config_name
+ * The configuration name.
+ * @param string $key
+ * The configuration key.
+ * @param string $value
+ * The configuration value to save.
+ */
+ protected function saveLanguageOverride($config_name, $key, $value) {
+ $translation_override = $this->languageManager
+ ->getLanguageConfigOverride($this->langcode, $config_name);
+ $translation_override
+ ->set($key, $value)
+ ->save();
+ $this->configFactory->reset($config_name);
+
+ $this->assertConfigValue($config_name, $key, $value);
+ }
+
+ /**
+ * Saves translation data from locale module.
+ *
+ * This will invoke LocaleConfigSubscriber through the event dispatcher. To
+ * make sure the configuration was persisted correctly, the configuration
+ * value is checked. Because LocaleConfigSubscriber temporarily disables the
+ * override state of the configuration factory we check that the correct value
+ * is restored afterwards.
+ *
+ * @param string $config_name
+ * The configuration name.
+ * @param string $key
+ * The configuration key.
+ * @param string $value
+ * The configuration value to save.
+ */
+ protected function saveLocaleTranslationData($config_name, $key, $value) {
+ $this->localeConfigManager
+ ->saveTranslationData($config_name, $this->langcode, [$key => $value]);
+ $this->configFactory->reset($config_name);
+
+ $this->assertConfigValue($config_name, $key, $value);
+ }
+
+ /**
+ * Deletes a language override.
+ *
+ * This will invoke LocaleConfigSubscriber through the event dispatcher. To
+ * make sure the configuration was persisted correctly, the configuration
+ * value is checked. Because LocaleConfigSubscriber temporarily disables the
+ * override state of the configuration factory we check that the correct value
+ * is restored afterwards.
+ *
+ * @param string $config_name
+ * The configuration name.
+ * @param string $key
+ * The configuration key.
+ * @param string $source_value
+ * The source configuration value to verify the correct value is returned
+ * from the configuration factory after the deletion.
+ */
+ protected function deleteLanguageOverride($config_name, $key, $source_value) {
+ $translation_override = $this->languageManager
+ ->getLanguageConfigOverride($this->langcode, $config_name);
+ $translation_override
+ ->clear($key)
+ ->save();
+ $this->configFactory->reset($config_name);
+
+ $this->assertConfigValue($config_name, $key, $source_value);
+ }
+
+ /**
+ * Deletes translation data from locale module.
+ *
+ * This will invoke LocaleConfigSubscriber through the event dispatcher. To
+ * make sure the configuration was persisted correctly, the configuration
+ * value is checked. Because LocaleConfigSubscriber temporarily disables the
+ * override state of the configuration factory we check that the correct value
+ * is restored afterwards.
+ *
+ * @param string $config_name
+ * The configuration name.
+ * @param string $key
+ * The configuration key.
+ * @param string $source_value
+ * The source configuration value to verify the correct value is returned
+ * from the configuration factory after the deletion.
+ */
+ protected function deleteLocaleTranslationData($config_name, $key, $source_value) {
+ $this->localeConfigManager->deleteTranslationData($config_name, $this->langcode);
+ $this->configFactory->reset($config_name);
+
+ $this->assertConfigValue($config_name, $key, $source_value);
+ }
+
+ /**
+ * Ensures configuration was saved correctly.
+ *
+ * @param $config_name
+ * The configuration name.
+ * @param $key
+ * The configuration key.
+ * @param $value
+ * The configuration value.
+ *
+ * @return bool
+ * TRUE if the assertion succeeded, FALSE otherwise.
+ */
+ protected function assertConfigValue($config_name, $key, $value) {
+ // Make sure the configuration was translated correctly.
+ $translation_config = $this->configFactory->get($config_name);
+ $passed = $this->assertIdentical($value, $translation_config->get($key));
+
+ // Make sure the override state of the configuration factory was not
+ // modified.
+ return $passed && $this->assertIdentical(TRUE, $this->configFactory->getOverrideState());
+ }
+
+ /**
+ * Ensures no translation exists.
+ *
+ * @param string $config_name
+ * The configuration name.
+ *
+ * @return bool
+ * TRUE if the assertion succeeded, FALSE otherwise.
+ */
+ protected function assertNoTranslation($config_name) {
+ $strings = $this->stringStorage->getTranslations([
+ 'type' => 'configuration',
+ 'name' => $config_name,
+ 'language' => $this->langcode,
+ 'translated' => TRUE,
+ ]);
+ return $this->assertIdentical([], $strings);
+ }
+
+ /**
+ * Ensures a translation exists and is marked as customized.
+ *
+ * @param string $config_name
+ * The configuration name.
+ * @param string $translation
+ * The translation.
+ * @param bool $customized
+ * Whether or not the string should be asserted to be customized or not
+ * customized.
+ *
+ * @return bool
+ * TRUE if the assertion succeeded, FALSE otherwise.
+ */
+ protected function assertTranslation($config_name, $translation, $customized = TRUE) {
+ // Make sure a string exists.
+ $strings = $this->stringStorage->getTranslations([
+ 'type' => 'configuration',
+ 'name' => $config_name,
+ 'language' => $this->langcode,
+ 'translated' => TRUE,
+ ]);
+ $pass = $this->assertIdentical(1, count($strings));
+ $string = reset($strings);
+ if ($this->assertTrue($string instanceof StringInterface)) {
+ /** @var \Drupal\locale\StringInterface $string */
+ $pass = $pass && $this->assertIdentical($translation, $string->getString());
+ $pass = $pass && $this->assertTrue($string->isTranslation());
+ if ($this->assertTrue($string instanceof TranslationString)) {
+ /** @var \Drupal\locale\TranslationString $string */
+ // Make sure the string is marked as customized so that it does not get
+ // overridden when the string translations are updated.
+ return $pass && $this->assertEqual($customized, $string->customized);
+ }
+ }
+ return FALSE;
+ }
+
+}
diff --git a/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php b/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php
index e0c20fc..3d26d37 100644
--- a/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php
+++ b/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php
@@ -136,8 +136,10 @@ class LocaleConfigTranslationTest extends WebTestBase {
// 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);
diff --git a/core/modules/locale/tests/modules/locale_test/config/schema/locale_test.schema.yml b/core/modules/locale/tests/modules/locale_test/config/schema/locale_test.schema.yml
index dd722be..5e7e056 100644
--- a/core/modules/locale/tests/modules/locale_test/config/schema/locale_test.schema.yml
+++ b/core/modules/locale/tests/modules/locale_test/config/schema/locale_test.schema.yml
@@ -7,6 +7,8 @@ locale_test.no_translation:
test:
type: string
label: 'Test'
+ # See \Drupal\locale\Tests\LocaleConfigSubscriberTest
+ translatable: true
locale_test.translation:
type: mapping
@@ -15,3 +17,5 @@ locale_test.translation:
test:
type: string
label: 'Test'
+ # See \Drupal\locale\Tests\LocaleConfigSubscriberTest
+ translatable: true