diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module index ef70fab..a109ac4 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -449,8 +449,6 @@ function locale_system_remove($components) { if ($language_list = locale_translatable_language_list()) { module_load_include('compare.inc', 'locale'); \Drupal::moduleHandler()->loadInclude('locale', 'bulk.inc'); - // Delete configuration translations. - Locale::config()->deleteComponentTranslations($components, array_keys($language_list)); // Only when projects are removed, the translation files and records will be // deleted. Not each disabled module will remove a project, e.g., sub diff --git a/core/modules/locale/src/LocaleConfigManager.php b/core/modules/locale/src/LocaleConfigManager.php index 1d1762f..1fa9e2f 100644 --- a/core/modules/locale/src/LocaleConfigManager.php +++ b/core/modules/locale/src/LocaleConfigManager.php @@ -7,36 +7,54 @@ namespace Drupal\locale; -use Drupal\Core\Config\TypedConfigManagerInterface; -use Drupal\Core\Config\StorageInterface; use Drupal\Core\Config\ConfigFactoryInterface; -use Drupal\Core\Language\LanguageInterface; -use Drupal\language\ConfigurableLanguageManagerInterface; +use Drupal\Core\Config\StorageInterface; +use Drupal\Core\Config\TypedConfigManagerInterface; +use Drupal\Core\StringTranslation\TranslationWrapper; use Drupal\Core\TypedData\TraversableTypedDataInterface; use Drupal\Core\TypedData\TypedDataInterface; -use Drupal\Core\StringTranslation\TranslationWrapper; +use Drupal\language\ConfigurableLanguageManagerInterface; /** - * Manages localized configuration type plugins. + * Manages configuration supported in part by interface translation. + * + * This manager is responsible to update configuration overrides and active + * translations when interface translation data changes. This allows Drupal to + * translate user roles, views, blocks, etc. after Drupal has been installed + * using the locale module's storage. When translations change in locale, + * LocaleConfigManager::updateConfigTranslations() is invoked to update the + * corresponding storage of the translation (in active storage or overrides). + * + * The saved changes result in configuration events to fire, which would make + * LocaleConfigSubscriber write back to the interface translation storage. To + * stop that from happening in case we initiate the writes from here, the + * $isUpdatingFromLocale flag is maintained, which is queried by + * LocaleConfigSubscriber. + * + * In turn when translated configuration or configuration language overrides are + * changed, it is the responsibility of LocaleConfigSubscriber to update locale + * storage. */ class LocaleConfigManager { /** - * A storage instance for reading configuration data. + * The storage instance for reading configuration data. * * @var \Drupal\Core\Config\StorageInterface */ protected $configStorage; /** - * A storage instance for reading default configuration data. + * The storage instance for reading default configuration data. * * @var \Drupal\Core\Config\StorageInterface */ protected $installStorage; /** - * A string storage for reading and writing translations. + * The string storage for reading and writing translations. + * + * @var \Drupal\locale\StringStorageInterface; */ protected $localeStorage; @@ -69,11 +87,17 @@ class LocaleConfigManager { protected $typedConfigManager; /** - * Whether or not configuration translations are currently being updated. + * Whether or not configuration translations are being updated from locale. + * + * In this case, LocaleConfigManager is in control of the process and the + * reference data is locale's storage. This is used to let + * LocaleConfigSubscriber know that it does not need to feed back to locale. + * On the other hand, when not updating from locale and configuration + * translations change, we need to feed back to the locale storage. * * @var bool */ - protected $isUpdating = FALSE; + protected $isUpdatingFromLocale = FALSE; /** * Creates a new typed configuration manager. @@ -218,9 +242,9 @@ protected function processTranslatableData($name, array $active, array $translat * Configuration data to be saved, that will be only the translated values. */ public function saveTranslationOverride($name, $langcode, array $data) { - $this->isUpdating = TRUE; + $this->isUpdatingFromLocale = TRUE; $this->languageManager->getLanguageConfigOverride($langcode, $name)->setData($data)->save(); - $this->isUpdating = FALSE; + $this->isUpdatingFromLocale = FALSE; } /** @@ -232,9 +256,9 @@ public function saveTranslationOverride($name, $langcode, array $data) { * Configuration data to be saved with translations merged in. */ public function saveTranslationActive($name, array $data) { - $this->isUpdating = TRUE; + $this->isUpdatingFromLocale = TRUE; $this->configFactory->getEditable($name)->setData($data)->save(); - $this->isUpdating = FALSE; + $this->isUpdatingFromLocale = FALSE; } /** @@ -246,9 +270,9 @@ public function saveTranslationActive($name, array $data) { * Language code. */ public function deleteTranslationOverride($name, $langcode) { - $this->isUpdating = TRUE; + $this->isUpdatingFromLocale = TRUE; $this->languageManager->getLanguageConfigOverride($langcode, $name)->delete(); - $this->isUpdating = FALSE; + $this->isUpdatingFromLocale = FALSE; } /** @@ -278,27 +302,6 @@ public function getComponentNames(array $components = array()) { } /** - * Deletes configuration translations for uninstalled components. - * - * @param array $components - * Array with string identifiers. - * @param array $langcodes - * Array of language codes. - */ - public function deleteComponentTranslations(array $components, array $langcodes) { - $this->isUpdating = TRUE; - $names = $this->getComponentNames($components); - if ($names && $langcodes) { - foreach ($names as $name) { - foreach ($langcodes as $langcode) { - $this->deleteTranslationOverride($name, $langcode); - } - } - } - $this->isUpdating = FALSE; - } - - /** * Gets configuration names associated with strings. * * @param array $lids @@ -323,12 +326,12 @@ public function getStringNames(array $lids) { * Language code to delete. */ public function deleteLanguageTranslations($langcode) { - $this->isUpdating = TRUE; + $this->isUpdatingFromLocale = TRUE; $storage = $this->languageManager->getLanguageConfigOverrideStorage($langcode); foreach ($storage->listAll() as $name) { $this->languageManager->getLanguageConfigOverride($langcode, $name)->delete(); } - $this->isUpdating = FALSE; + $this->isUpdatingFromLocale = FALSE; } /** @@ -462,29 +465,16 @@ public function isSupported($name) { } /** - * Whether the given configuration is supported for interface translation. - * - * @param $name - * The configuration name. - * - * @return bool - * TRUE if the configuration both exists as default and active configuration - * and their language codes don't match. - */ - public function isTranslatedConfig($name) { - $default_langcode = $this->defaultConfigLangcode($name); - $active_langcode = $this->activeConfigLangcode($name); - return !is_null($default_langcode) && !is_null($active_langcode) && $default_langcode != $active_langcode; - } - - /** * Indicates whether configuration translations are currently being updated. * * @return bool * Whether or not configuration translations are currently being updated. + * If TRUE, LocaleConfigManager is in control of the process and the + * reference data is locale's storage. Changes made to active configuration + * and overrides in this case should not feed back to locale storage. */ - public function isUpdatingConfigTranslations() { - return $this->isUpdating; + public function isUpdatingTranslationsFromLocale() { + return $this->isUpdatingFromLocale; } /** @@ -523,7 +513,7 @@ public function updateConfigTranslations(array $names, array $langcodes = array( if ($langcode != $active_langcode) { // If the language code is not the same as the active storage // language, we should update a configuration override. - if ($processed) { + if (!empty($processed)) { // Update translation data in configuration override. $this->saveTranslationOverride($name, $langcode, $processed); $count++; diff --git a/core/modules/locale/src/LocaleConfigSubscriber.php b/core/modules/locale/src/LocaleConfigSubscriber.php index d76db9d..13ed0f8 100644 --- a/core/modules/locale/src/LocaleConfigSubscriber.php +++ b/core/modules/locale/src/LocaleConfigSubscriber.php @@ -6,37 +6,45 @@ namespace Drupal\locale; -use Drupal\Core\Config\Config; +use Drupal\Core\Config\ConfigCrudEvent; use Drupal\Core\Config\ConfigEvents; use Drupal\Core\Config\ConfigFactoryInterface; -use Drupal\Core\Config\ConfigInstallerEvent; -use Drupal\Core\Config\StorageInterface; +use Drupal\Core\Config\StorableConfigBase; use Drupal\Core\Language\LanguageManagerInterface; -use Drupal\Core\TypedData\TraversableTypedDataInterface; -use Drupal\language\Config\LanguageConfigOverride; use Drupal\language\Config\LanguageConfigOverrideCrudEvent; use Drupal\language\Config\LanguageConfigOverrideEvents; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** - * Updates corresponding string translation when language overrides change. + * Updates strings translation when configuration translations change. * - * This reacts to the updating or deleting of configuration language overrides. - * It checks whether there are string translations associated with the - * configuration that is being saved and, if so, updates those string - * translations with the new configuration values and marks them as customized. - * That way manual updates to configuration will not be inadvertently reverted - * when updated translations from https://localize.drupal.org are being - * imported. + * This reacts to the updates of translated active configuration and + * configuration language overrides. When those updates involve configuration + * which was available as default configuration, we need to feed back changes + * to any item which was originally part of that configuration to the interface + * translation storage. Those updated translations are saved as customized, so + * further community translation updates will not undo user changes. + * + * The active configuration or configuration language override changes may + * be initiated by locale data changes themselves. In that case, + * LocaleConfigManager is the initiator and it maintains a flag to consult, so + * we can ignore the changes in those cases, because such changes made should + * already be up to date in locale storage. + * + * This subscriber does not respond to deleting active configuration or deleting + * configuration translations. The locale storage is additive and we cannot be + * sure that only a given configuration translation used a source string. So + * we should not remove the translations from locale storage in these cases. The + * configuration or override would itself be deleted either way. */ class LocaleConfigSubscriber implements EventSubscriberInterface { /** - * The string storage. + * The string storage for reading and writing translations. * * @var \Drupal\locale\StringStorageInterface; */ - protected $stringStorage; + protected $localeStorage; /** * The configuration factory. @@ -70,7 +78,7 @@ class LocaleConfigSubscriber implements EventSubscriberInterface { * The typed configuration manager. */ public function __construct(StringStorageInterface $string_storage, ConfigFactoryInterface $config_factory, LocaleConfigManager $locale_config_manager, LanguageManagerInterface $language_manager) { - $this->stringStorage = $string_storage; + $this->localeStorage = $string_storage; $this->configFactory = $config_factory; $this->localeConfigManager = $locale_config_manager; $this->languageManager = $language_manager; @@ -81,123 +89,79 @@ public function __construct(StringStorageInterface $string_storage, ConfigFactor */ public static function getSubscribedEvents() { $events[LanguageConfigOverrideEvents::SAVE_OVERRIDE] = 'onOverrideSave'; - $events[LanguageConfigOverrideEvents::DELETE_OVERRIDE] = 'onOverrideDelete'; + $events[ConfigEvents::SAVE] = 'onConfigSave'; return $events; } /** - * Updates the translation strings when shipped configuration is saved. + * Updates the locale strings when a translated active configuration is saved. * - * @param \Drupal\language\Config\LanguageConfigOverrideCrudEvent $event - * The language configuration event. + * @param \Drupal\Core\Config\ConfigCrudEvent $event + * The configuration event. */ - public function onOverrideSave(LanguageConfigOverrideCrudEvent $event) { - // Do not mark strings as customized when community translations are being - // imported. - if ($this->localeConfigManager->isUpdatingConfigTranslations()) { - $callable = [$this, 'saveTranslation']; - } - else { - $callable = [$this, 'saveCustomizedTranslation']; + public function onConfigSave(ConfigCrudEvent $event) { + // Only attempt to feed back configuration translation changes to locale if + // the update itself was not initiated by locale data changes. + if (!$this->localeConfigManager->isUpdatingTranslationsFromLocale()) { + $config = $event->getConfig(); + $this->updateLocaleStorage($config); } - - $this->updateTranslationStrings($event, $callable); } /** - * Updates the translation strings when shipped configuration is deleted. + * Updates the locale strings when a configuration override is saved. * * @param \Drupal\language\Config\LanguageConfigOverrideCrudEvent $event * The language configuration event. */ - public function onOverrideDelete(LanguageConfigOverrideCrudEvent $event) { - if ($this->localeConfigManager->isUpdatingConfigTranslations()) { - $callable = [$this, 'deleteTranslation']; - } - else { - // Do not delete the string, but save a customized translation with the - // source value so that the deletion will not be reverted by importing - // community translations. - // @see \Drupal\locale\LocaleConfigSubscriber::saveCustomizedTranslation() - $callable = [$this, 'saveCustomizedTranslation']; + public function onOverrideSave(LanguageConfigOverrideCrudEvent $event) { + // Only attempt to feed back configuration override changes to locale if + // the update itself was not initiated by locale data changes. + if (!$this->localeConfigManager->isUpdatingTranslationsFromLocale()) { + $translation_config = $event->getLanguageConfigOverride(); + $this->updateLocaleStorage($translation_config); } - - $this->updateTranslationStrings($event, $callable); } /** - * Updates the translation strings of shipped configuration. + * Update locale storage based on configuration translations. * - * @param \Drupal\language\Config\LanguageConfigOverrideCrudEvent $event - * The language configuration event. - * @param $callable - * A callable to apply to each translatable string of the configuration. + * @param \Drupal\Core\Config\StorableConfigBase $config + * Active configuration or configuration translation override. */ - protected function updateTranslationStrings(LanguageConfigOverrideCrudEvent $event, $callable) { - return; - $translation_config = $event->getLanguageConfigOverride(); - $name = $translation_config->getName(); + protected function updateLocaleStorage(StorableConfigBase $config) { + $name = $config->getName(); + $langcode = $config->getLangcode(); - // Only do anything if the configuration was shipped. - if ($this->stringStorage->getLocations(['type' => 'configuration', 'name' => $name])) { - $source_config = $this->configFactory->getEditable($name); - $schema = $this->localeConfigManager->getTranslatableDefaultConfig($name)->getTypedConfig(); - $this->traverseSchema($schema, $source_config, $translation_config, $callable); + if ($this->localeConfigManager->isSupported($name) && ($langcode != 'en' || locale_translate_english())) { + $translatables = $this->localeConfigManager->getTranslatableDefaultConfig($name); + $this->processTranslatableData($name, $config, $translatables, $langcode); } } /** - * Traverses configuration schema and applies a callback to each leaf element. - * - * It skips leaf elements that are not translatable. - * - * @param \Drupal\Core\TypedData\TraversableTypedDataInterface $schema - * The respective configuration schema. - * @param callable $callable - * The callable to apply to each leaf element. The callable will be called - * with the leaf element and the element key as arguments. - * @param string|null $base_key - * (optional) The base key that the schema belongs to. This should be NULL - * for the top-level schema and be populated consecutively when recursing - * into the schema structure. + * Process the translatable data array with a given language. + * + * @param string $name + * The configuration name. + * @param array $active + * The active configuration data or override data. + * @param array $translatable + * The translatable array structure, see this::getTranslatableData(). + * @param string $langcode + * The language code to process the array with. */ - protected function traverseSchema(TraversableTypedDataInterface $schema, Config $source_config, LanguageConfigOverride $translation_config, $callable, $base_key = NULL) { - foreach ($schema as $key => $element) { - $element_key = implode('.', array_filter([$base_key, $key])); - - // We only care for strings here, so traverse the schema further in the - // case of traversable elements. - if ($element instanceof TraversableTypedDataInterface) { - $this->traverseSchema($element, $source_config, $translation_config, $callable, $element_key); + protected function processTranslatableData($name, array $active, array $translatable, $langcode) { + foreach ($translatable as $key => $item) { + if (!isset($active[$key])) { + continue; } - // Skip elements which are not translatable. - elseif (!empty($element->getDataDefinition()['translatable'])) { - $callable( - $source_config->get($element_key), - $translation_config->getLangcode(), - $translation_config->get($element_key) - ); + if (is_array($item)) { + $this->processTranslatableData($name, $active[$key], $item, $langcode); } - } - } - - /** - * Saves a translation string. - * - * @param string $source_value - * The source string value. - * @param string $langcode - * The language code of the translation. - * @param string|null $translation_value - * (optional) The translation string value. If omitted, no translation will - * be saved. - */ - protected function saveTranslation($source_value, $langcode, $translation_value = NULL) { - if ($translation_value && ($translation = $this->getTranslation($source_value, $langcode, TRUE))) { - if ($translation->isNew() || $translation->getString() != $translation_value) { - $translation - ->setString($translation_value) - ->save(); + else { + /** @var \Drupal\Core\StringTranslation\TranslationWrapper $item */ + $this->saveCustomizedTranslation($name, $item->getString(), $item->getOption('context'), $active[$key], $langcode); } } } @@ -205,77 +169,32 @@ protected function saveTranslation($source_value, $langcode, $translation_value /** * Saves a translation string and marks it as customized. * - * @param string $source_value + * @param string $name + * The configuration name. + * @param string $source * The source string value. + * @param string $context + * The source string context. + * @param string $translation + * The translation string. * @param string $langcode * The language code of the translation. - * @param string|null $translation_value - * (optional) The translation string value. If omitted, a customized string - * with the source value will be saved. - * - * @see \Drupal\locale\LocaleConfigSubscriber::onOverrideDelete() */ - protected function saveCustomizedTranslation($source_value, $langcode, $translation_value = NULL) { - if ($translation = $this->getTranslation($source_value, $langcode, TRUE)) { - if (!isset($translation_value)) { - $translation_value = $source_value; - } - if ($translation->isNew() || $translation->getString() != $translation_value) { - $translation - ->setString($translation_value) + protected function saveCustomizedTranslation($name, $source, $context, $translation, $langcode) { + // If the source is still the same as the translation, keep it as-is, to let + // locale to later update translations. + if ($source != $translation) { + $locale_translation = $this->localeConfigManager->translateString($name, $langcode, $source, $context); + // If the translation is the same as we already have in locale, keep it + // as-is, ie. don't set customized. + if ($translation != $locale_translation) { + $string = $this->localeStorage->findTranslation(array('source' => $source, 'context' => $context, 'language' => $langcode)); + $string + ->setString($translation) ->setCustomized(TRUE) ->save(); } } } - /** - * Deletes a translation string, if it exists. - * - * @param string $source_value - * The source string value. - * @param string $langcode - * The language code of the translation. - * - * @see \Drupal\locale\LocaleConfigSubscriber::onOverrideDelete() - */ - protected function deleteTranslation($source_value, $langcode) { - if ($translation = $this->getTranslation($source_value, $langcode, FALSE)) { - $translation->delete(); - } - } - - /** - * Gets a translation string. - * - * @param string $source_value - * The source string value. - * @param string $langcode - * The language code of the translation. - * @param bool $create_fallback - * (optional) By default if a source string could be found and no - * translation in the given language exists yet, a translation object is - * created. This can be circumvented by passing FALSE. - * - * @return \Drupal\locale\TranslationString|null - * The translation string if one was found or created. - */ - protected function getTranslation($source_value, $langcode, $create_fallback = TRUE) { - // There is no point in creating a translation without a source. - if ($source_string = $this->stringStorage->findString(['source' => $source_value])) { - // Get the translation for this original source string from locale. - $conditions = [ - 'lid' => $source_string->lid, - 'language' => $langcode, - ]; - $translations = $this->stringStorage->getTranslations($conditions + ['translated' => TRUE]); - if ($translations) { - return reset($translations); - } - elseif ($create_fallback) { - return $this->stringStorage->createTranslation($conditions); - } - } - } - }