diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index cc50e0f..3818159 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -312,3 +312,33 @@ condition.plugin:
label: 'Context assignments'
sequence:
- type: string
+
+# Human readable string that is associated with a format.
+text_format:
+ type: mapping
+ label: 'Text with text format'
+ # Even though it is not sensible to translate the text format of a formatted
+ # string, we conceive of the text and its date format as a single composite
+ # object and declare that object (or in other words the entire mapping) as
+ # translatable. This causes the entire mapping to be saved to the language
+ # overrides of the configuration. Storing only the (to be formatted) text
+ # could result in security problems in case the text format of the source
+ # text is changed.
+ translatable: true
+ mapping:
+ value:
+ type: text
+ label: 'Text'
+ # locale.module integrates the language overrides of shipped configuration
+ # with https://localize.drupal.org. Because it only handles strings and
+ # cannot deal with complex data structures, it parses the configuration
+ # schema until it reaches a primitive and only then checks whether the
+ # element is translatable.
+ translatable: true
+ format:
+ type: string
+ label: 'Text format'
+ # Even though the entire 'text_format' is marked as translatable for the
+ # sake of language configuration overrides, the ID of the text format of
+ # texts in shipped configuration should not be exposed to
+ # https://localize.drupal.org
diff --git a/core/lib/Drupal/Core/Config/ConfigEvents.php b/core/lib/Drupal/Core/Config/ConfigEvents.php
index 7fa22a5..2b2d2c8 100644
--- a/core/lib/Drupal/Core/Config/ConfigEvents.php
+++ b/core/lib/Drupal/Core/Config/ConfigEvents.php
@@ -9,6 +9,8 @@
/**
* Defines events for the configuration system.
+ *
+ * @see \Drupal\Core\Config\ConfigCrudEvent
*/
final class ConfigEvents {
@@ -21,6 +23,18 @@
const SAVE = 'config.save';
/**
+ * Name of event fired when saving the configuration override.
+ *
+ * This event is not used by the configuration system itself but should be
+ * used by implementors of configuration overrides. See Language module's
+ * implementation for an example.
+ *
+ * @see \Drupal\Core\Config\ConfigOverrideCrudEvent
+ * @see \Drupal\language\Config\LanguageConfigOverride::save()
+ */
+ const SAVE_OVERRIDE = 'config.save_override';
+
+ /**
* Name of event fired when deleting the configuration object.
*
* @see \Drupal\Core\Config\Config::delete()
@@ -28,6 +42,18 @@
const DELETE = 'config.delete';
/**
+ * Name of event fired when deleting the configuration override.
+ *
+ * This event is not used by the configuration system itself but should be
+ * used by implementors of configuration overrides. See Language module's
+ * implementation for an example.
+ *
+ * @see \Drupal\Core\Config\ConfigOverrideCrudEvent
+ * @see \Drupal\language\Config\LanguageConfigOverride::delete()
+ */
+ const DELETE_OVERRIDE = 'config.delete_override';
+
+ /**
* Name of event fired when renaming a configuration object.
*
* @see \Drupal\Core\Config\ConfigFactoryInterface::rename().
diff --git a/core/lib/Drupal/Core/Config/ConfigOverrideCrudEvent.php b/core/lib/Drupal/Core/Config/ConfigOverrideCrudEvent.php
new file mode 100644
index 0000000..377221c
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/ConfigOverrideCrudEvent.php
@@ -0,0 +1,45 @@
+config = $config;
+ }
+
+ /**
+ * Gets configuration object.
+ *
+ * @return \Drupal\Core\Config\StorableConfigBase
+ * The configuration object that caused the event to fire.
+ */
+ public function getConfig() {
+ return $this->config;
+ }
+
+}
+
diff --git a/core/lib/Drupal/Core/Config/TypedConfigManagerInterface.php b/core/lib/Drupal/Core/Config/TypedConfigManagerInterface.php
index f475c02..2c59287 100644
--- a/core/lib/Drupal/Core/Config/TypedConfigManagerInterface.php
+++ b/core/lib/Drupal/Core/Config/TypedConfigManagerInterface.php
@@ -24,7 +24,7 @@
* @param string $name
* Configuration object name.
*
- * @return \Drupal\Core\Config\Schema\Element
+ * @return \Drupal\Core\Config\Schema\ArrayElement
* 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 6504b86..9720a18 100644
--- a/core/modules/config_translation/config_translation.module
+++ b/core/modules/config_translation/config_translation.module
@@ -189,12 +189,30 @@ function config_translation_entity_operation(EntityInterface $entity) {
/**
* Implements hook_config_translation_type_info_alter().
+ *
+ * @todo Convert this hook into a real typed data alter hook,
+ * https://drupal.org/node/2145633.
*/
function config_translation_config_translation_type_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',
+ );
+
// Enhance the text and date type definitions with classes to generate proper
// form elements in ConfigTranslationFormBase. Other translatable types will
// appear as a one line textfield.
- $definitions['text']['form_element_class'] = '\Drupal\config_translation\FormElement\Textarea';
- $definitions['date_format']['form_element_class'] = '\Drupal\config_translation\FormElement\DateFormat';
+ foreach ($definitions as $type => &$definition) {
+ if (!isset($definition['form_element_class'])) {
+ if (isset($map[$definition['type']])) {
+ $definition['form_element_class'] = $map[$definition['type']];
+ }
+ elseif (!empty($definition['translatable'])) {
+ $definition['form_element_class'] = '\Drupal\config_translation\FormElement\Textfield';
+ }
+ }
+ }
}
diff --git a/core/modules/config_translation/src/Form/ConfigTranslationFormBase.php b/core/modules/config_translation/src/Form/ConfigTranslationFormBase.php
index 5b6125a..331652b 100644
--- a/core/modules/config_translation/src/Form/ConfigTranslationFormBase.php
+++ b/core/modules/config_translation/src/Form/ConfigTranslationFormBase.php
@@ -9,15 +9,14 @@
use Drupal\config_translation\ConfigMapperManagerInterface;
use Drupal\Core\Config\Config;
+use Drupal\Core\Config\Schema\ArrayElement;
use Drupal\Core\Config\Schema\Element;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\BaseFormIdInterface;
use Drupal\Core\Form\FormBase;
-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;
@@ -42,13 +41,6 @@
protected $configMapperManager;
/**
- * The string translation storage object.
- *
- * @var \Drupal\locale\StringStorageInterface
- */
- protected $localeStorage;
-
- /**
* The module handler to invoke the alter hook.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
@@ -91,21 +83,20 @@
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\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook.
+ * @param \Drupal\language\ConfigurableLanguageManagerInterface $language_manager
+ * The configurable language manager.
*/
- public function __construct(TypedConfigManagerInterface $typed_config_manager, ConfigMapperManagerInterface $config_mapper_manager, StringStorageInterface $locale_storage, ModuleHandlerInterface $module_handler, ConfigurableLanguageManagerInterface $language_manager) {
+ public function __construct(TypedConfigManagerInterface $typed_config_manager, ConfigMapperManagerInterface $config_mapper_manager, ModuleHandlerInterface $module_handler, ConfigurableLanguageManagerInterface $language_manager) {
$this->typedConfigManager = $typed_config_manager;
$this->configMapperManager = $config_mapper_manager;
- $this->localeStorage = $locale_storage;
$this->moduleHandler = $module_handler;
$this->languageManager = $language_manager;
}
@@ -117,7 +108,6 @@ public static function create(ContainerInterface $container) {
return new static(
$container->get('config.typed'),
$container->get('plugin.manager.config_translation.mapper'),
- $container->get('locale.storage'),
$container->get('module_handler'),
$container->get('language_manager')
);
@@ -195,7 +185,7 @@ public function buildForm(array $form, array &$form_state, Request $request = NU
);
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]);
+ $form['config_names'][$name] += $this->buildConfigForm($name, $this->typedConfigManager->get($name), $config_factory->get($name)->get(), $this->baseConfigData[$name]);
}
$form['actions']['#type'] = 'actions';
@@ -215,7 +205,7 @@ public function buildForm(array $form, array &$form_state, Request $request = NU
* {@inheritdoc}
*/
public function submitForm(array &$form, array &$form_state) {
- $form_values = $form_state['values']['config_names'];
+ $form_values = $form_state['values']['translation']['config_names'];
// For the form submission handling, use the raw data.
$config_factory = $this->configFactory();
@@ -226,9 +216,8 @@ public function submitForm(array &$form, array &$form_state) {
// Set configuration values based on form submission and source values.
$base_config = $config_factory->get($name);
$config_translation = $this->languageManager->getLanguageConfigOverride($this->language->id, $name);
- $locations = $this->localeStorage->getLocations(array('type' => 'configuration', 'name' => $name));
- $this->setConfig($this->language, $base_config, $config_translation, $form_values[$name], !empty($locations));
+ $this->setConfig($base_config, $config_translation, $this->typedConfigManager->get($name), $form_values[$name]);
// If no overrides, delete language specific configuration file.
$saved_config = $config_translation->get();
@@ -250,7 +239,9 @@ public function submitForm(array &$form, array &$form_state) {
/**
* Formats configuration schema as a form tree.
*
- * @param \Drupal\Core\Config\Schema\Element $schema
+ * @param string $name
+ * The configuration name.
+ * @param \Drupal\Core\Config\Schema\ArrayElement $schema
* Schema definition of configuration.
* @param array|string $config_data
* Configuration object of requested language, a string when done traversing
@@ -262,12 +253,15 @@ public function submitForm(array &$form, array &$form_state) {
* (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.
+ * (optional) The base key that the schema and the configuration values
+ * belong to. This should be NULL for the top-level configuration object and
+ * be populated consecutively when recursing into the configuration
+ * structure.
*
* @return array
* An associative array containing the structure of the form.
*/
- protected function buildConfigForm(Element $schema, $config_data, $base_config_data, $open = TRUE, $base_key = '') {
+ protected function buildConfigForm($name, ArrayElement $schema, $config_data, $base_config_data, $open = TRUE, $base_key = NULL) {
$build = array();
foreach ($schema as $key => $element) {
// Make the specific element key, "$base_key.$key".
@@ -276,10 +270,47 @@ protected function buildConfigForm(Element $schema, $config_data, $base_config_d
if (!$definition->getLabel()) {
$definition->setLabel($this->t('N/A'));
}
- if ($element instanceof Element) {
+ // Invoke hook_config_translation_type_info_alter() implementations to
+ // alter the configuration types.
+ $definitions = array(
+ $definition['type'] => &$definition,
+ );
+
+ $this->moduleHandler->alter('config_translation_type_info', $definitions);
+
+ // If this element is translatable, show a translation form. This may be a
+ // simple element, as is the case for unformatted text or date formats, or
+ // an array element, as is the case for formatted text. In the latter case
+ // the form element class is responsible for handling input for all
+ // configuration values contained in the element.
+ if (!empty($definition['translatable'])) {
+ /** @var \Drupal\config_translation\FormElement\ElementInterface $form_element */
+ $form_element = new $definition['form_element_class']();
+
+ $build[$element_key] = array(
+ '#theme' => 'config_translation_manage_form_element',
+ );
+ $build[$element_key]['source'] = $form_element->getSourceElement($definition, $this->sourceLanguage, $base_config_data[$key]);
+ $build[$element_key]['translation'] = $form_element->getTranslationElement($definition, $this->language, $base_config_data[$key], $config_data[$key]);
+ // 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'].
+ $parents = array_merge(array('config_names', $name), explode('.', $element_key));
+ $build[$element_key]['source']['#parents'] = array_merge(array('source'), $parents);
+ $build[$element_key]['translation']['#parents'] = array_merge(array('translation'), $parents);
+
+ }
+ // If this is a non-translatable array element, traverse the schema
+ // further.
+ elseif ($element instanceof ArrayElement) {
// 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);
+ $sub_build = $this->buildConfigForm($name, $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.).
@@ -308,45 +339,8 @@ protected function buildConfigForm(Element $schema, $config_data, $base_config_d
) + $sub_build;
}
}
- else {
- $definition = $element->getDataDefinition();
-
- // Invoke hook_config_translation_type_info_alter() implementations to
- // alter the configuration types.
- $definitions = array(
- $definition['type'] => &$definition,
- );
- $this->moduleHandler->alter('config_translation_type_info', $definitions);
-
- // Create form element only for translatable items.
- if (!isset($definition['translatable']) || !isset($definition['type'])) {
- continue;
- }
-
- $value = $config_data[$key];
- $build[$element_key] = array(
- '#theme' => 'config_translation_manage_form_element',
- );
- $build[$element_key]['source'] = array(
- '#markup' => $base_config_data[$key] ? ('' . nl2br($base_config_data[$key] . '')) : t('(Empty)'),
- '#title' => $this->t(
- '!label (!source_language)',
- array(
- '!label' => $this->t($definition['label']),
- '!source_language' => $this->sourceLanguage->name,
- )
- ),
- '#type' => 'item',
- );
-
- if (!isset($definition['form_element_class'])) {
- $definition['form_element_class'] = '\Drupal\config_translation\FormElement\Textfield';
- }
-
- /** @var \Drupal\config_translation\FormElement\ElementInterface $form_element */
- $form_element = new $definition['form_element_class']();
- $build[$element_key]['translation'] = $form_element->getFormElement($definition, $this->language, $value);
- }
+ // If this is a simple, non-translatable element, simply continue with the
+ // next element.
}
return $build;
}
@@ -354,12 +348,12 @@ protected function buildConfigForm(Element $schema, $config_data, $base_config_d
/**
* 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 \Drupal\Core\Config\Schema\ArrayElement $schema
+ * Schema definition of configuration.
* @param array $config_values
* A simple one dimensional or recursive array:
* - simple:
@@ -371,51 +365,40 @@ protected function buildConfigForm(Element $schema, $config_data, $base_config_d
* );
* 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.
+ * @param string|null $base_key
+ * (optional) The base key that the schema and the configuration values
+ * belong to. This should be NULL for the top-level configuration object and
+ * be populated consecutively when recursing into the configuration
+ * structure.
*
* @return array
* Translation configuration override data.
*/
- 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'])) {
+ protected function setConfig(Config $base_config, LanguageConfigOverride $config_translation, ArrayElement $schema, array $config_values, $base_key = NULL) {
+ foreach ($schema as $key => $element) {
+ if (!isset($config_values[$key])) {
+ continue;
+ }
+ $value = $config_values[$key];
+
+ $element_key = implode('.', array_filter(array($base_key, $key)));
+ $definition = $element->getDataDefinition();
+ // While the 'form_element_class' key is used for form building, the
+ // 'translatable' key is used for the setting of configuration values.
+ // This allows the form to contain values which will not be set, such as
+ // the TextFormat element's 'format' value.
+ if ($element instanceof Element && empty($definition['translatable'])) {
// Traverse into this level in the configuration.
- $this->setConfig($language, $base_config, $config_translation, $value, $shipped_config);
+ $this->setConfig($base_config, $config_translation, $element, $value, $element_key);
}
else {
-
- // If the configuration file being translated was originally shipped, we
- // should update the locale translation storage. The string should
- // already be there, but we make sure to check.
- if ($shipped_config && $source_string = $this->localeStorage->findString(array('source' => $base_config->get($key)))) {
-
- // Get the translation for this original source string from locale.
- $conditions = array(
- 'lid' => $source_string->lid,
- 'language' => $language->id,
- );
- $translations = $this->localeStorage->getTranslations($conditions + array('translated' => TRUE));
- // If we got a translation, take that, otherwise create a new one.
- $translation = reset($translations) ?: $this->localeStorage->createTranslation($conditions);
-
- // If we have a new translation or different from what is stored in
- // locale before, save this as an updated customize translation.
- if ($translation->isNew() || $translation->getString() != $value['translation']) {
- $translation->setString($value['translation'])
- ->setCustomized()
- ->save();
- }
- }
-
// Save value, if different from the source value in the base
// configuration. If same as original configuration, remove override.
- if ($base_config->get($key) !== $value['translation']) {
- $config_translation->set($key, $value['translation']);
+ if ($base_config->get($element_key) !== $value) {
+ $config_translation->set($element_key, $value);
}
else {
- $config_translation->clear($key);
+ $config_translation->clear($element_key);
}
}
}
diff --git a/core/modules/config_translation/src/FormElement/DateFormat.php b/core/modules/config_translation/src/FormElement/DateFormat.php
index 25858cf..952ce4f 100644
--- a/core/modules/config_translation/src/FormElement/DateFormat.php
+++ b/core/modules/config_translation/src/FormElement/DateFormat.php
@@ -11,34 +11,30 @@
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\ReplaceCommand;
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(DataDefinitionInterface $definition, LanguageInterface $translation_language, $source_config, $translation_config) {
$description = $this->t('A user-defined date format. See the PHP manual for available options.', array('@url' => 'http://php.net/manual/function.date.php'));
- $format = $this->t('Displayed as %date_format', array('%date_format' => \Drupal::service('date')->format(REQUEST_TIME, 'custom', $value)));
+ $format = $this->t('Displayed as %date_format', array('%date_format' => \Drupal::service('date')->format(REQUEST_TIME, 'custom', $translation_config)));
+
return array(
'#type' => 'textfield',
- '#title' => $this->t($definition['label']) . ' (' . $language->name . ')',
'#description' => $description,
- '#default_value' => $value,
- '#attributes' => array('lang' => $language->id),
'#field_suffix' => '
' . $format . '
',
'#ajax' => array(
'callback' => 'Drupal\config_translation\FormElement\DateFormat::ajaxSample',
'event' => 'keyup',
'progress' => array('type' => 'throbber', 'message' => NULL),
),
- );
+ ) + parent::getTranslationElement($definition, $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..78a89b6 100644
--- a/core/modules/config_translation/src/FormElement/ElementInterface.php
+++ b/core/modules/config_translation/src/FormElement/ElementInterface.php
@@ -16,19 +16,78 @@
interface ElementInterface {
/**
+ * Returns the source element for a given configuration definition.
+ *
+ * This can be either a renderable 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 array $definition
+ * The configuration schema for the element.
+ * @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.
+ */
+ public function getSourceElement(array $definition, LanguageInterface $source_language, $source_config);
+
+ /**
* 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\TypedData\DataDefinitionInterface $definition
- * Configuration schema type definition of the element.
+ * The configuration schema for 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.
+ * 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()
*/
- public function getFormElement(DataDefinitionInterface $definition, LanguageInterface $language, $value);
-
+ public function getTranslationElement(DataDefinitionInterface $definition, LanguageInterface $language, $source_config, $translation_config);
}
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..55713cb
--- /dev/null
+++ b/core/modules/config_translation/src/FormElement/FormElementBase.php
@@ -0,0 +1,58 @@
+getId() . '">' . nl2br($source_config) . '';
+ }
+ else {
+ $value = $this->t('(Empty)');
+ }
+
+ return array(
+ '#type' => 'item',
+ '#title' => $this->t('!label (!source_language)', array(
+ '!label' => $this->t($definition['label']),
+ '!source_language' => $source_language->getName(),
+ )),
+ '#markup' => $value,
+ );
+ }
+
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTranslationElement(DataDefinitionInterface $definition, LanguageInterface $translation_language, $source_config, $translation_config) {
+ // Add basic properties that apply to all form elements.
+ return array(
+ '#title' => $this->t('!label (!source_language)', array(
+ '!label' => $this->t($definition['label']),
+ '!source_language' => $translation_language->getName(),
+ )),
+ '#default_value' => $translation_config,
+ '#attributes' => array('lang' => $translation_language->getId()),
+ );
+ }
+
+}
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..0477d16
--- /dev/null
+++ b/core/modules/config_translation/src/FormElement/TextFormat.php
@@ -0,0 +1,46 @@
+getTranslationElement($definition, $source_language, $source_config, $source_config) + array(
+ '#value' => $source_config['value'],
+ '#disabled' => TRUE,
+ '#allow_focus' => TRUE,
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTranslationElement(DataDefinitionInterface $definition, 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'],
+ '#allowed_formats' => array($source_config['format']),
+ ) + parent::getTranslationElement($definition, $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 8092e4b..2dfe9aa 100644
--- a/core/modules/config_translation/src/FormElement/Textarea.php
+++ b/core/modules/config_translation/src/FormElement/Textarea.php
@@ -8,31 +8,26 @@
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(DataDefinitionInterface $definition, 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['label']) . ' (' . $language->name . ')',
'#rows' => $rows,
- '#attributes' => array('lang' => $language->id),
- );
+ ) + parent::getTranslationElement($definition, $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 a4a820f..3ebd72b 100644
--- a/core/modules/config_translation/src/FormElement/Textfield.php
+++ b/core/modules/config_translation/src/FormElement/Textfield.php
@@ -8,25 +8,20 @@
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(DataDefinitionInterface $definition, LanguageInterface $translation_language, $source_config, $translation_config) {
return array(
'#type' => 'textfield',
- '#default_value' => $value,
- '#title' => $this->t($definition['label']) . ' (' . $language->name . ')',
- '#attributes' => array('lang' => $language->id),
- );
+ ) + parent::getTranslationElement($definition, $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 c5bce3f..591c796 100644
--- a/core/modules/config_translation/src/Tests/ConfigTranslationFormTest.php
+++ b/core/modules/config_translation/src/Tests/ConfigTranslationFormTest.php
@@ -20,7 +20,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 08e28e3..41dd224 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;
@@ -24,7 +25,7 @@ class ConfigTranslationUiTest extends WebTestBase {
*
* @var array
*/
- public static $modules = array('node', 'contact', 'config_translation', 'config_translation_test', 'views', 'views_ui', 'contextual');
+ public static $modules = array('node', 'contact', 'config_translation', 'config_translation_test', 'views', 'views_ui', 'contextual', 'filter', 'filter_test');
/**
* Languages to enable.
@@ -67,12 +68,24 @@ public function setUp() {
$translator_permissions = array(
'translate configuration',
);
+
+ /** @var \Drupal\filter\FilterFormatInterface $filter_test_format */
+ $filter_test_format = entity_load('filter_format', 'filter_test');
+ /** @var \Drupal\filter\FilterFormatInterface $filtered_html_format */
+ $filtered_html_format = entity_load('filter_format', 'filtered_html');
+ /** @var \Drupal\filter\FilterFormatInterface $full_html_format */
+ $full_html_format = entity_load('filter_format', 'full_html');
+
$admin_permissions = array_merge(
$translator_permissions,
array(
'administer languages',
'administer site configuration',
'administer contact forms',
+ 'administer filters',
+ $filtered_html_format->getPermissionName(),
+ $full_html_format->getPermissionName(),
+ $filter_test_format->getPermissionName(),
'access site-wide contact form',
'access contextual links',
'administer views',
@@ -93,7 +106,7 @@ public function setUp() {
/**
* Tests the site information translation interface.
*/
- public function testSiteInformationTranslationUi() {
+ public function _testSiteInformationTranslationUi() {
$this->drupalLogin($this->admin_user);
$site_name = 'Site name for testing configuration translation';
@@ -128,8 +141,8 @@ public function testSiteInformationTranslationUi() {
// Update site name and slogan for French.
$edit = array(
- 'config_names[system.site][name][translation]' => $fr_site_name,
- 'config_names[system.site][slogan][translation]' => $fr_site_slogan,
+ 'translation[config_names][system.site][name]' => $fr_site_name,
+ 'translation[config_names][system.site][slogan]' => $fr_site_slogan,
);
$this->drupalPostForm("$translation_base_url/fr/add", $edit, t('Save translation'));
@@ -142,8 +155,8 @@ public function testSiteInformationTranslationUi() {
// Check translation saved proper.
$this->drupalGet("$translation_base_url/fr/edit");
- $this->assertFieldByName('config_names[system.site][name][translation]', $fr_site_name);
- $this->assertFieldByName('config_names[system.site][slogan][translation]', $fr_site_slogan);
+ $this->assertFieldByName('translation[config_names][system.site][name]', $fr_site_name);
+ $this->assertFieldByName('translation[config_names][system.site][slogan]', $fr_site_slogan);
// Check French translation of site name and slogan are in place.
$this->drupalGet('fr');
@@ -159,7 +172,7 @@ public function testSiteInformationTranslationUi() {
/**
* Tests the site information translation interface.
*/
- public function testSourceValueDuplicateSave() {
+ public function _testSourceValueDuplicateSave() {
$this->drupalLogin($this->admin_user);
$site_name = 'Site name for testing configuration translation';
@@ -171,8 +184,8 @@ public function testSourceValueDuplicateSave() {
// Case 1: Update new value for site slogan and site name.
$edit = array(
- 'config_names[system.site][name][translation]' => 'FR ' . $site_name,
- 'config_names[system.site][slogan][translation]' => 'FR ' . $site_slogan,
+ 'translation[config_names][system.site][name]' => 'FR ' . $site_name,
+ 'translation[config_names][system.site][slogan]' => 'FR ' . $site_slogan,
);
// First time, no overrides, so just Add link.
$this->drupalPostForm("$translation_base_url/fr/add", $edit, t('Save translation'));
@@ -194,8 +207,8 @@ public function testSourceValueDuplicateSave() {
$this->assertNoText('FR ' . $site_name);
$this->assertNoText('FR ' . $site_slogan);
$edit = array(
- 'config_names[system.site][name][translation]' => $site_name,
- 'config_names[system.site][slogan][translation]' => 'FR ' . $site_slogan,
+ 'translation[config_names][system.site][name]' => $site_name,
+ 'translation[config_names][system.site][slogan]' => 'FR ' . $site_slogan,
);
$this->drupalPostForm(NULL, $edit, t('Save translation'));
$this->assertRaw(t('Successfully updated @language translation.', array('@language' => 'French')));
@@ -209,8 +222,8 @@ public function testSourceValueDuplicateSave() {
$this->drupalGet("$translation_base_url/fr/edit");
$this->assertNoText('FR ' . $site_slogan);
$edit = array(
- 'config_names[system.site][name][translation]' => $site_name,
- 'config_names[system.site][slogan][translation]' => $site_slogan,
+ 'translation[config_names][system.site][name]' => $site_name,
+ 'translation[config_names][system.site][slogan]' => $site_slogan,
);
$this->drupalPostForm(NULL, $edit, t('Save translation'));
$override = \Drupal::languageManager()->getLanguageConfigOverride('fr', 'system.site');
@@ -236,7 +249,7 @@ public function testSourceValueDuplicateSave() {
/**
* Tests the contact category translation.
*/
- public function testContactConfigEntityTranslation() {
+ public function _testContactConfigEntityTranslation() {
$this->drupalLogin($this->admin_user);
$this->drupalGet('admin/structure/contact');
@@ -274,8 +287,8 @@ public function testContactConfigEntityTranslation() {
// Update translatable fields.
$edit = array(
- 'config_names[contact.category.feedback][label][translation]' => 'Website feedback - ' . $langcode,
- 'config_names[contact.category.feedback][reply][translation]' => 'Thank you for your mail - ' . $langcode,
+ 'translation[config_names][contact.category.feedback][label]' => 'Website feedback - ' . $langcode,
+ 'translation[config_names][contact.category.feedback][reply]' => 'Thank you for your mail - ' . $langcode,
);
// Save language specific version of form.
@@ -313,7 +326,7 @@ public function testContactConfigEntityTranslation() {
$langcode_prefixes = array_merge(array(''), $this->langcodes);
foreach ($langcode_prefixes as $langcode_prefix) {
$this->drupalGet(ltrim("$langcode_prefix/$translation_base_url/$langcode/edit"));
- $this->assertFieldByName('config_names[contact.category.feedback][label][translation]', 'Website feedback - ' . $langcode);
+ $this->assertFieldByName('translation[config_names][contact.category.feedback][label]', 'Website feedback - ' . $langcode);
$this->assertText($label);
}
}
@@ -367,7 +380,7 @@ public function testContactConfigEntityTranslation() {
/**
* Tests date format translation.
*/
- public function testDateFormatTranslation() {
+ public function _testDateFormatTranslation() {
$this->drupalLogin($this->admin_user);
$this->drupalGet('admin/config/regional/date-time');
@@ -403,8 +416,8 @@ public function testDateFormatTranslation() {
// Update translatable fields.
$edit = array(
- 'config_names[system.date_format.' . $id . '][label][translation]' => $id . ' - FR',
- 'config_names[system.date_format.' . $id . '][pattern][translation]' => 'D',
+ 'translation[config_names][system.date_format.' . $id . '][label]' => $id . ' - FR',
+ 'translation[config_names][system.date_format.' . $id . '][pattern]' => 'D',
);
// Save language specific version of form.
@@ -432,7 +445,7 @@ public function testDateFormatTranslation() {
* names involved building up one configuration translation form. Test that
* the translations are saved for all configuration names properly.
*/
- public function testAccountSettingsConfigurationTranslation() {
+ public function _testAccountSettingsConfigurationTranslation() {
$this->drupalLogin($this->admin_user);
$this->drupalGet('admin/config/people/accounts/translate');
@@ -440,9 +453,9 @@ public function testAccountSettingsConfigurationTranslation() {
// Update account settings fields for French.
$edit = array(
- 'config_names[user.settings][anonymous][translation]' => 'Anonyme',
- 'config_names[user.mail][status_blocked][status_blocked.subject][translation]' => 'Testing, your account is blocked.',
- 'config_names[user.mail][status_blocked][status_blocked.body][translation]' => 'Testing account blocked body.',
+ 'translation[config_names][user.settings][anonymous]' => 'Anonyme',
+ 'translation[config_names][user.mail][status_blocked][subject]' => 'Testing, your account is blocked.',
+ 'translation[config_names][user.mail][status_blocked][body]' => 'Testing account blocked body.',
);
$this->drupalPostForm('admin/config/people/accounts/translate/fr/add', $edit, t('Save translation'));
@@ -451,7 +464,7 @@ public function testAccountSettingsConfigurationTranslation() {
$this->drupalGet('admin/config/people/accounts/translate/fr/edit');
foreach ($edit as $key => $value) {
// Check the translations appear in the right field type as well.
- $xpath = '//' . (strpos($key, '.body') ? 'textarea' : 'input') . '[@name="'. $key . '"]';
+ $xpath = '//' . (strpos($key, '[body]') ? 'textarea' : 'input') . '[@name="'. $key . '"]';
$this->assertFieldByXPath($xpath, $value);
}
// Check that labels for email settings appear.
@@ -462,7 +475,7 @@ public function testAccountSettingsConfigurationTranslation() {
/**
* Tests source and target language edge cases.
*/
- public function testSourceAndTargetLanguage() {
+ public function _testSourceAndTargetLanguage() {
$this->drupalLogin($this->admin_user);
// Loading translation page for not-specified language (und)
@@ -510,7 +523,7 @@ public function testSourceAndTargetLanguage() {
/**
* Tests the views translation interface.
*/
- public function testViewsTranslationUI() {
+ public function _testViewsTranslationUI() {
$this->drupalLogin($this->admin_user);
// Assert contextual link related to views.
@@ -538,10 +551,10 @@ public function testViewsTranslationUI() {
// Update Views Fields for French.
$edit = array(
- 'config_names[views.view.frontpage][description][translation]' => $description . " FR",
- 'config_names[views.view.frontpage][label][translation]' => $human_readable_name . " FR",
- 'config_names[views.view.frontpage][display][default][display.default.display_title][translation]' => $display_settings_master . " FR",
- 'config_names[views.view.frontpage][display][default][display_options][display.default.display_options.title][translation]' => $display_options_master . " FR",
+ 'translation[config_names][views.view.frontpage][description]' => $description . " FR",
+ 'translation[config_names][views.view.frontpage][label]' => $human_readable_name . " FR",
+ 'translation[config_names][views.view.frontpage][display][default][display_title]' => $display_settings_master . " FR",
+ 'translation[config_names][views.view.frontpage][display][default][display_options][title]' => $display_options_master . " FR",
);
$this->drupalPostForm("$translation_base_url/fr/add", $edit, t('Save translation'));
$this->assertRaw(t('Successfully saved @language translation.', array('@language' => 'French')));
@@ -553,16 +566,16 @@ public function testViewsTranslationUI() {
// Check translation saved proper.
$this->drupalGet("$translation_base_url/fr/edit");
- $this->assertFieldByName('config_names[views.view.frontpage][description][translation]', $description . " FR");
- $this->assertFieldByName('config_names[views.view.frontpage][label][translation]', $human_readable_name . " FR");
- $this->assertFieldByName('config_names[views.view.frontpage][display][default][display.default.display_title][translation]', $display_settings_master . " FR");
- $this->assertFieldByName('config_names[views.view.frontpage][display][default][display_options][display.default.display_options.title][translation]', $display_options_master . " FR");
+ $this->assertFieldByName('translation[config_names][views.view.frontpage][description]', $description . " FR");
+ $this->assertFieldByName('translation[config_names][views.view.frontpage][label]', $human_readable_name . " FR");
+ $this->assertFieldByName('translation[config_names][views.view.frontpage][display][default][display_title]', $display_settings_master . " FR");
+ $this->assertFieldByName('translation[config_names][views.view.frontpage][display][default][display_options][title]', $display_options_master . " FR");
}
/**
* Test translation storage in locale storage.
*/
- public function testLocaleDBStorage() {
+ public function _testLocaleDBStorage() {
// Enable import of translations. By default this is disabled for automated
// tests.
\Drupal::config('locale.settings')
@@ -587,7 +600,7 @@ public function testLocaleDBStorage() {
// Add custom translation.
$edit = array(
- 'config_names[user.settings][anonymous][translation]' => 'Anonyme',
+ 'translation[config_names][user.settings][anonymous]' => 'Anonyme',
);
$this->drupalPostForm('admin/config/people/accounts/translate/fr/add', $edit, t('Save translation'));
@@ -598,7 +611,7 @@ public function testLocaleDBStorage() {
// revert custom translations to base translation.
$edit = array(
- 'config_names[user.settings][anonymous][translation]' => 'Anonymous',
+ 'translation[config_names][user.settings][anonymous]' => 'Anonymous',
);
$this->drupalPostForm('admin/config/people/accounts/translate/fr/edit', $edit, t('Save translation'));
@@ -610,7 +623,7 @@ public function testLocaleDBStorage() {
/**
* Tests the single language existing.
*/
- public function testSingleLanguageUI() {
+ public function _testSingleLanguageUI() {
$this->drupalLogin($this->admin_user);
// Delete French language
@@ -637,7 +650,7 @@ public function testSingleLanguageUI() {
/**
* Tests the config_translation_info_alter() hook.
*/
- public function testAlterInfo() {
+ public function _testAlterInfo() {
$this->drupalLogin($this->admin_user);
$this->container->get('state')->set('config_translation_test_config_translation_info_alter', TRUE);
@@ -656,6 +669,104 @@ public function testAlterInfo() {
}
/**
+ * Test text_format translation.
+ */
+ public function testTextFormatTranslation() {
+ $this->drupalLogin($this->admin_user);
+ /** @var \Drupal\Core\Config\ConfigFactoryInterface $config_factory */
+ $config_factory = $this->container->get('config.factory');
+
+ $expected = array(
+ 'value' => 'Hello World',
+ '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]' => 'Hello World - FR',
+ );
+
+ // 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' => 'Hello World - FR',
+ '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' => 'Hello World - FR',
+ '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
@@ -723,4 +834,33 @@ protected function renderContextualLinks($ids, $current_path) {
return $this->drupalPost('contextual/render', 'application/json', $post, array('query' => array('destination' => $current_path)));
}
+ /**
+ * Asserts that a textarea with a given ID has been disabled from editing.
+ *
+ * @param string $id
+ * The HTML ID of the textarea.
+ *
+ * @return bool
+ * TRUE if the assertion passed; FALSE otherwise.
+ */
+ protected function assertDisabledTextarea($id) {
+ $textarea = $this->xpath('//textarea[@id=:id and contains(@disabled, "disabled")]', array(
+ ':id' => $id,
+ ));
+ $textarea = reset($textarea);
+ $passed = $this->assertTrue($textarea instanceof \SimpleXMLElement, String::format('Disabled field @id exists.', array(
+ '@id' => $id,
+ )));
+ $expected = 'This field has been disabled because you do not have sufficient permissions to edit it.';
+ $passed = $passed && $this->assertEqual((string) $textarea, $expected, String::format('Disabled textarea @id hides text in an inaccessible text format.', array(
+ '@id' => $id,
+ )));
+ // Make sure the text format select is not shown.
+ $select_id = str_replace('value', 'format--2', $id);
+ $select = $this->xpath('//select[@id=:id]', array(':id' => $select_id));
+ return $passed && $this->assertFalse($select, String::format('Field @id does not exist.', array(
+ '@id' => $id,
+ )));
+ }
+
}
diff --git a/core/modules/config_translation/tests/modules/config_translation_test/config/install/config_translation_test.content.yml b/core/modules/config_translation/tests/modules/config_translation_test/config/install/config_translation_test.content.yml
new file mode 100644
index 0000000..55aa430
--- /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: "Hello World"
+ 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.local_tasks.yml b/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.local_tasks.yml
new file mode 100644
index 0000000..92581b4
--- /dev/null
+++ b/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.local_tasks.yml
@@ -0,0 +1,7 @@
+# Add a default local task for the file system settings page, so that the local
+# task added by Configuration Translation becomes visible. This facilitates
+# manual testing.
+system.file_system_settings:
+ route_name: system.file_system_settings
+ title: Settings
+ base_route: system.file_system_settings
diff --git a/core/modules/config_translation/tests/themes/config_translation_test_theme/config_translation_test_theme.config_translation.yml b/core/modules/config_translation/tests/themes/config_translation_test_theme/config_translation_test_theme.config_translation.yml
index 7c8cdd8..5d84070 100644
--- a/core/modules/config_translation/tests/themes/config_translation_test_theme/config_translation_test_theme.config_translation.yml
+++ b/core/modules/config_translation/tests/themes/config_translation_test_theme/config_translation_test_theme.config_translation.yml
@@ -1,3 +1,4 @@
+# Attach to performance settings for testing. The base route does not matter.
system.performance_settings:
title: 'Theme translation test'
base_route_name: system.performance_settings
diff --git a/core/modules/language/src/Config/LanguageConfigFactoryOverride.php b/core/modules/language/src/Config/LanguageConfigFactoryOverride.php
index 6c71478..3539400 100644
--- a/core/modules/language/src/Config/LanguageConfigFactoryOverride.php
+++ b/core/modules/language/src/Config/LanguageConfigFactoryOverride.php
@@ -93,7 +93,15 @@ public function loadOverrides($names) {
public function getOverride($langcode, $name) {
$storage = $this->getStorage($langcode);
$data = $storage->read($name);
- $override = new LanguageConfigOverride($name, $storage, $this->typedConfigManager);
+
+ $override = new LanguageConfigOverride(
+ $name,
+ $langcode,
+ $storage,
+ $this->typedConfigManager,
+ $this->eventDispatcher
+ );
+
if (!empty($data)) {
$override->initWithData($data);
}
diff --git a/core/modules/language/src/Config/LanguageConfigOverride.php b/core/modules/language/src/Config/LanguageConfigOverride.php
index 19fb549..11a9ed7 100644
--- a/core/modules/language/src/Config/LanguageConfigOverride.php
+++ b/core/modules/language/src/Config/LanguageConfigOverride.php
@@ -7,9 +7,12 @@
namespace Drupal\language\Config;
+use Drupal\Core\Config\ConfigEvents;
+use Drupal\Core\Config\ConfigOverrideCrudEvent;
use Drupal\Core\Config\StorableConfigBase;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Config\TypedConfigManagerInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Defines language configuration overrides.
@@ -17,20 +20,40 @@
class LanguageConfigOverride extends StorableConfigBase {
/**
+ * The language code of this language override.
+ *
+ * @var string
+ */
+ protected $langcode;
+
+ /**
+ * The event dispatcher.
+ *
+ * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+ */
+ protected $eventDispatcher;
+
+ /**
* Constructs a language override object.
*
* @param string $name
* The name of the configuration object being overridden.
+ * @param string $langcode
+ * The language code of the language of this language override.
* @param \Drupal\Core\Config\StorageInterface $storage
* A storage controller object to use for reading and writing the
* configuration override.
* @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config
* The typed configuration manager service.
+ * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
+ * The event dispatcher.
*/
- public function __construct($name, StorageInterface $storage, TypedConfigManagerInterface $typed_config) {
+ public function __construct($name, $langcode, StorageInterface $storage, TypedConfigManagerInterface $typed_config, EventDispatcherInterface $event_dispatcher) {
$this->name = $name;
+ $this->langcode = $langcode;
$this->storage = $storage;
$this->typedConfigManager = $typed_config;
+ $this->eventDispatcher = $event_dispatcher;
}
/**
@@ -45,6 +68,7 @@ public function save() {
}
$this->storage->write($this->name, $this->data);
$this->isNew = FALSE;
+ $this->eventDispatcher->dispatch(ConfigEvents::SAVE_OVERRIDE, new ConfigOverrideCrudEvent($this));
$this->originalData = $this->data;
return $this;
}
@@ -56,8 +80,19 @@ public function delete() {
$this->data = array();
$this->storage->delete($this->name);
$this->isNew = TRUE;
+ $this->eventDispatcher->dispatch(ConfigEvents::DELETE_OVERRIDE, new ConfigOverrideCrudEvent($this));
$this->originalData = $this->data;
return $this;
}
+ /**
+ * Returns the language code of this language override.
+ *
+ * @return string
+ * The language code.
+ */
+ public function getLangcode() {
+ return $this->langcode;
+ }
+
}
diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc
index ba5f497..ba161d6 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.
@@ -627,18 +628,26 @@ function locale_config_batch_finished($success, array $results) {
* Number of configuration objects retranslated.
*/
function locale_config_update_multiple(array $names, $langcodes = array()) {
+ /** @var \Drupal\language\ConfigurableLanguageManagerInterface $language_manager */
+ $language_manager = \Drupal::languageManager();
+ $locale_config_manager = Locale::config();
+
$langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list());
$count = 0;
foreach ($names as $name) {
- $wrapper = \Drupal\locale\Locale::config()->get($name);
+ $wrapper = $locale_config_manager->get($name);
foreach ($langcodes as $langcode) {
$translation = $wrapper->getValue() ? $wrapper->getTranslation($langcode)->getValue() : NULL;
if ($translation) {
- \Drupal\locale\Locale::config()->saveTranslationData($name, $langcode, $translation);
+ $locale_config_manager->saveTranslationData($name, $langcode, $translation);
$count++;
}
else {
- \Drupal\locale\Locale::config()->deleteTranslationData($name, $langcode);
+ // Do not bother deleting language overrides which do not exist in the
+ // first place.
+ if (!$language_manager->getLanguageConfigOverride($langcode, $name)->isNew()) {
+ $locale_config_manager->deleteTranslationData($name, $langcode);
+ }
}
}
}
diff --git a/core/modules/locale/locale.services.yml b/core/modules/locale/locale.services.yml
index f7794c3..a9ea7ea 100644
--- a/core/modules/locale/locale.services.yml
+++ b/core/modules/locale/locale.services.yml
@@ -11,3 +11,8 @@ services:
tags:
- { name: string_translator }
- { name: needs_destruction }
+ locale.config_subscriber:
+ class: Drupal\locale\LocaleConfigSubscriber
+ arguments: ['@locale.storage', '@config.factory', '@locale.config.typed']
+ tags:
+ - { name: event_subscriber }
diff --git a/core/modules/locale/src/LocaleConfigSubscriber.php b/core/modules/locale/src/LocaleConfigSubscriber.php
new file mode 100644
index 0000000..651651b
--- /dev/null
+++ b/core/modules/locale/src/LocaleConfigSubscriber.php
@@ -0,0 +1,159 @@
+stringStorage = $string_storage;
+ $this->configFactory = $config_factory;
+ $this->localeConfigManager = $locale_config_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getSubscribedEvents() {
+ // Instead of deleting the actual translation strings we save empty strings
+ // when the configuration override gets deleted, so we can re-use the same
+ // function for both events.
+ $events[ConfigEvents::SAVE_OVERRIDE] = 'onOverrideUpdate';
+ $events[ConfigEvents::DELETE_OVERRIDE] = 'onOverrideUpdate';
+ return $events;
+ }
+
+
+ /**
+ * Updates the translation strings of shipped configuration.
+ *
+ * @param \Drupal\Core\Config\ConfigOverrideCrudEvent $event
+ */
+ public function onOverrideUpdate(ConfigOverrideCrudEvent $event) {
+ $translation_config = $event->getConfig();
+ $name = $translation_config->getName();
+
+ if (
+ // Only react to language overrides.
+ $translation_config instanceof LanguageConfigOverride &&
+ // Only do anything if the configuration was shipped.
+ $this->stringStorage->getLocations(array(
+ 'type' => 'configuration',
+ 'name' => $name,
+ ))
+ ) {
+ $source_config = $this->configFactory->get($name);
+ $schema = $this->localeConfigManager->get($name)->getTypedConfig();
+ $this->saveStrings($source_config, $translation_config, $schema);
+ }
+ }
+
+ /**
+ * Updates strings for a certain config element.
+ *
+ * @param \Drupal\Core\Config\Config $source_config
+ * The source configuration.
+ * @param \Drupal\language\Config\LanguageConfigOverride $translation_config
+ * The language configuration override.
+ * @param \Drupal\Core\Config\Schema\ArrayElement $schema
+ * The respective configuration schema.
+ * @param string|null $base_key
+ * (optional) The base key that the schema and the configuration values
+ * belong to. This should be NULL for the top-level configuration object and
+ * be populated consecutively when recursing into the configuration
+ * structure.
+ */
+ protected function saveStrings(Config $source_config, LanguageConfigOverride $translation_config, ArrayElement $schema, $base_key = NULL) {
+ foreach ($schema as $key => $element) {
+ $element_key = implode('.', array_filter(array($base_key, $key)));
+
+ // We only care for strings here, so traverse the schema further in the
+ // case of array elements.
+ if ($element instanceof ArrayElement) {
+ $this->saveStrings($source_config, $translation_config, $element, $element_key);
+ }
+ else {
+ $definition = $element->getDataDefinition();
+ $source_value = $source_config->get($element_key);
+
+ // Ignore this value if it is not translatable or if no source string
+ // can be found.
+ if (
+ !empty($definition['translatable']) &&
+ $source_string = $this->stringStorage->findString(array('source' => $source_value))
+ ) {
+ // Get the translation for this original source string from locale.
+ $conditions = array(
+ 'lid' => $source_string->lid,
+ 'language' => $translation_config->getLangcode(),
+ );
+ $translations = $this->stringStorage->getTranslations($conditions + array('translated' => TRUE));
+ // If we got a translation, take that, otherwise create a new one.
+ $translation = reset($translations) ?: $this->stringStorage->createTranslation($conditions);
+
+ // If we have a new translation or different from what is stored in
+ // locale before, save this as an updated customize translation.
+ $value = $translation_config->get($element_key);
+ // If there is no value, save the source value as the translation.
+ // This has the same effect as deleting the string wholesale (which
+ // would be more correct) but ensures that the translation does not
+ // get re-imported when updating translations.
+ if (!isset($value)) {
+ $value = $source_value;
+ }
+ if ($translation->isNew() || $translation->getString() != $value) {
+ $translation->setString($value)
+ ->setCustomized()
+ ->save();
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php b/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php
index 3abe65a..05094b1 100644
--- a/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php
+++ b/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php
@@ -139,8 +139,10 @@ function testConfigTranslation() {
// Check the string is unique and has no translation yet.
$translations = $this->storage->getTranslations(array('language' => $langcode, 'type' => 'configuration', 'name' => 'image.style.medium'));
+ $this->assertEqual(count($translations), 1);
$translation = reset($translations);
- $this->assertTrue(count($translations) == 1 && $translation->source == $string->source && empty($translation->translation), 'Got only one string for image configuration and has no translation.');
+ $this->assertEqual($translation->source, $string->source);
+ $this->assertTrue(empty($translation->translation));
// Translate using the UI so configuration is refreshed.
$image_style_label = $this->randomName(20);