diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index b8e122b..b906f72 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -316,11 +316,28 @@ condition.plugin: 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 http://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 + # http://localize.drupal.org diff --git a/core/lib/Drupal/Core/Config/ConfigEvents.php b/core/lib/Drupal/Core/Config/ConfigEvents.php index 7fa22a5..a4b6446 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\ConfiguCrudEvent */ 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 827363f..c4138ff 100644 --- a/core/lib/Drupal/Core/Config/TypedConfigManagerInterface.php +++ b/core/lib/Drupal/Core/Config/TypedConfigManagerInterface.php @@ -23,7 +23,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/src/Form/ConfigTranslationFormBase.php b/core/modules/config_translation/src/Form/ConfigTranslationFormBase.php index 115d3bc..a0e20d5 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') ); @@ -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, $this->typedConfigManager->get($name), $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(); @@ -252,7 +241,7 @@ public function submitForm(array &$form, array &$form_state) { * * @param string $name * The configuration name. - * @param \Drupal\Core\Config\Schema\Element $schema + * @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 @@ -264,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($name, 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". @@ -283,16 +275,37 @@ protected function buildConfigForm($name, Element $schema, $config_data, $base_c ); $this->moduleHandler->alter('config_translation_type_info', $definitions); - $element_type = $definition['type']; - - // When building the form we traverse the schema until we find an element - // with a form element class. When setting the config values in, we - // instead traverse the schema until we find a translatable element. This - // allows for for elements handling multiple schema parts only some of - // which are translatable. The TextFormat element requires this as the - // form inherently covers the 'format' property as well, even though that - // is not translatable. - if ($element instanceof Element && !isset($definitions[$element_type]['form_element_class'])) { + + // 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, $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($name, $element, $config_data[$key], $base_config_data[$key], FALSE, $element_key); @@ -324,28 +337,8 @@ protected function buildConfigForm($name, Element $schema, $config_data, $base_c ) + $sub_build; } } - elseif (isset($definition['form_element_class'])) { - /** @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]); - // Make sure that the source does not end up in the form values. The - // 'item' element, for example, generally receives input, - $build[$element_key]['source']['#input'] = FALSE; - - $build[$element_key]['translation'] = $form_element->getTranslationElement($definition, $this->language, $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 in - // ConfigTranslationFormBase::setConfig(). Therefore, the 'translation' - // key is removed from the form parents. - $build[$element_key]['translation']['#parents'] = array_merge(array('config_names', $name), explode('.', $element_key)); - } + // If this is a simple, non-translatable element, simply continue with the + // next element. } return $build; } @@ -353,13 +346,11 @@ protected function buildConfigForm($name, Element $schema, $config_data, $base_c /** * 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\Element $schema + * @param \Drupal\Core\Config\Schema\ArrayElement $schema * Schema definition of configuration. * @param array $config_values * A simple one dimensional or recursive array: @@ -372,15 +363,16 @@ protected function buildConfigForm($name, Element $schema, $config_data, $base_c * ); * 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. * - * @param null $base_key * @return array * Translation configuration override data. */ - protected function setConfig(LanguageInterface $language, LanguageConfigOverride $base_config, Config $config_translation, Element $schema, array $config_values, $shipped_config = FALSE, $base_key = NULL) { + 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; @@ -395,39 +387,16 @@ protected function setConfig(LanguageInterface $language, LanguageConfigOverride // 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, $element, $value, $shipped_config, $element_key); + $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->setString($value) - ->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) { + 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/lib/Drupal/config_translation/FormElement/FormElementBase.php b/core/modules/config_translation/src/FormElement/FormElementBase.php similarity index 100% rename from core/modules/config_translation/lib/Drupal/config_translation/FormElement/FormElementBase.php rename to core/modules/config_translation/src/FormElement/FormElementBase.php diff --git a/core/modules/config_translation/lib/Drupal/config_translation/FormElement/TextFormat.php b/core/modules/config_translation/src/FormElement/TextFormat.php similarity index 76% rename from core/modules/config_translation/lib/Drupal/config_translation/FormElement/TextFormat.php rename to core/modules/config_translation/src/FormElement/TextFormat.php index 4b5d14a..451c204 100644 --- a/core/modules/config_translation/lib/Drupal/config_translation/FormElement/TextFormat.php +++ b/core/modules/config_translation/src/FormElement/TextFormat.php @@ -24,6 +24,7 @@ public function getTranslationElement(array $definition, LanguageInterface $lang '#type' => 'text_format', '#default_value' => $value['value'], '#format' => $value['format'], + '#allowed_formats' => array($value['format']), ) + parent::getTranslationElement($definition, $language, $value); } @@ -39,13 +40,6 @@ public function getSourceElement(array $definition, LanguageInterface $source_la '#disabled' => TRUE, '#allow_focus' => TRUE, ); - // Because #input is set to FALSE in - // ConfigTranslationFormBase::buildConfigForm(), - // FormBuilder::handleInputElement() is not called on this element. - // Therefore we need to set the appropriate attribute manually. - $element['#attributes']['readonly'] = 'readonly'; - // @todo CKEditor does not support the 'readonly' attribute. - $element['#attributes']['disabled'] = 'disabled'; return $element; } diff --git a/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php b/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php index 73f25ba..27422fd 100644 --- a/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php +++ b/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php @@ -128,8 +128,8 @@ public function testSiteInformationTranslationUi() { // Update site name and slogan for French. $edit = array( - 'config_names[system.site][name]' => $fr_site_name, - 'config_names[system.site][slogan]' => $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 +142,8 @@ public function testSiteInformationTranslationUi() { // Check translation saved proper. $this->drupalGet("$translation_base_url/fr/edit"); - $this->assertFieldByName('config_names[system.site][name]', $fr_site_name); - $this->assertFieldByName('config_names[system.site][slogan]', $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'); @@ -171,8 +171,8 @@ public function testSourceValueDuplicateSave() { // Case 1: Update new value for site slogan and site name. $edit = array( - 'config_names[system.site][name]' => 'FR ' . $site_name, - 'config_names[system.site][slogan]' => '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 +194,8 @@ public function testSourceValueDuplicateSave() { $this->assertNoText('FR ' . $site_name); $this->assertNoText('FR ' . $site_slogan); $edit = array( - 'config_names[system.site][name]' => $site_name, - 'config_names[system.site][slogan]' => '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 +209,8 @@ public function testSourceValueDuplicateSave() { $this->drupalGet("$translation_base_url/fr/edit"); $this->assertNoText('FR ' . $site_slogan); $edit = array( - 'config_names[system.site][name]' => $site_name, - 'config_names[system.site][slogan]' => $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'); @@ -274,8 +274,8 @@ public function testContactConfigEntityTranslation() { // Update translatable fields. $edit = array( - 'config_names[contact.category.feedback][label]' => 'Website feedback - ' . $langcode, - 'config_names[contact.category.feedback][reply]' => '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 +313,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]', 'Website feedback - ' . $langcode); + $this->assertFieldByName('translation[config_names][contact.category.feedback][label]', 'Website feedback - ' . $langcode); $this->assertText($label); } } @@ -403,8 +403,8 @@ public function testDateFormatTranslation() { // Update translatable fields. $edit = array( - 'config_names[system.date_format.' . $id . '][label]' => $id . ' - FR', - 'config_names[system.date_format.' . $id . '][pattern]' => '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. @@ -440,9 +440,9 @@ public function testAccountSettingsConfigurationTranslation() { // Update account settings fields for French. $edit = array( - 'config_names[user.settings][anonymous]' => 'Anonyme', - 'config_names[user.mail][status_blocked][subject]' => 'Testing, your account is blocked.', - 'config_names[user.mail][status_blocked][body]' => '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')); @@ -538,10 +538,10 @@ public function testViewsTranslationUI() { // Update Views Fields for French. $edit = array( - 'config_names[views.view.frontpage][description]' => $description . " FR", - 'config_names[views.view.frontpage][label]' => $human_readable_name . " FR", - 'config_names[views.view.frontpage][display][default][display_title]' => $display_settings_master . " FR", - 'config_names[views.view.frontpage][display][default][display_options][title]' => $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,10 +553,10 @@ public function testViewsTranslationUI() { // Check translation saved proper. $this->drupalGet("$translation_base_url/fr/edit"); - $this->assertFieldByName('config_names[views.view.frontpage][description]', $description . " FR"); - $this->assertFieldByName('config_names[views.view.frontpage][label]', $human_readable_name . " FR"); - $this->assertFieldByName('config_names[views.view.frontpage][display][default][display_title]', $display_settings_master . " FR"); - $this->assertFieldByName('config_names[views.view.frontpage][display][default][display_options][title]', $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"); } /** @@ -587,7 +587,7 @@ public function testLocaleDBStorage() { // Add custom translation. $edit = array( - 'config_names[user.settings][anonymous]' => 'Anonyme', + 'translation[config_names][user.settings][anonymous]' => 'Anonyme', ); $this->drupalPostForm('admin/config/people/accounts/translate/fr/add', $edit, t('Save translation')); @@ -598,7 +598,7 @@ public function testLocaleDBStorage() { // revert custom translations to base translation. $edit = array( - 'config_names[user.settings][anonymous]' => 'Anonymous', + 'translation[config_names][user.settings][anonymous]' => 'Anonymous', ); $this->drupalPostForm('admin/config/people/accounts/translate/fr/edit', $edit, t('Save translation')); @@ -684,7 +684,7 @@ public function testTextFormatTranslation() { // Update translatable fields. $edit = array( - 'config_names[config_translation_test.content][content][value]' => 'Hello World - FR', + 'translation[config_names][config_translation_test.content][content][value]' => 'Hello World - FR', ); // Save language specific version of form. 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..4d97644 --- /dev/null +++ b/core/modules/locale/src/LocaleConfigSubscriber.php @@ -0,0 +1,161 @@ +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);