diff --git a/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php b/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php index 1285cb7..c69b705 100644 --- a/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php +++ b/core/lib/Drupal/Core/StringTranslation/TranslationWrapper.php @@ -60,6 +60,29 @@ public function __construct($string, array $arguments = array(), array $options } /** + * Get the string value stored in this translation wrapper. + * + * @return string + * The string stored in this wrapper. + */ + public function getString() { + return $this->string; + } + + /** + * Get a specific option from this translation wrapper. + * + * @param $name + * Option name. + * + * @return mixed + * The value of this option or empty string of option is not set. + */ + public function getOption($name) { + return isset($this->options[$name]) ? $this->options[$name] : ''; + } + + /** * Implements the magic __toString() method. */ public function __toString() { diff --git a/core/modules/config_translation/src/ConfigNamesMapper.php b/core/modules/config_translation/src/ConfigNamesMapper.php index b681b33..e771d3a 100644 --- a/core/modules/config_translation/src/ConfigNamesMapper.php +++ b/core/modules/config_translation/src/ConfigNamesMapper.php @@ -465,7 +465,7 @@ public function hasTranslatable() { */ public function hasTranslation(LanguageInterface $language) { foreach ($this->getConfigNames() as $name) { - if ($this->localeConfigManager->hasTranslation($name, $language)) { + if ($this->localeConfigManager->hasTranslation($name, $language->getId())) { return TRUE; } } diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc index 357aaf8..a4677ca 100644 --- a/core/modules/locale/locale.bulk.inc +++ b/core/modules/locale/locale.bulk.inc @@ -324,7 +324,7 @@ function locale_translate_batch_refresh(array &$context) { } elseif ($name = array_shift($context['sandbox']['refresh']['names'])) { // Refresh all languages for one object at a time. - $count = locale_config_update_multiple(array($name), $context['sandbox']['refresh']['languages']); + $count = Locale::config()->updateConfigTranslations(array($name), $context['sandbox']['refresh']['languages']); $context['results']['stats']['config'] += $count; // Inherit finished information from the "parent" string lookup step so // visual display of status will make sense. @@ -532,7 +532,7 @@ function locale_translate_delete_translation_files(array $projects = array(), ar * The batch definition. */ function locale_config_batch_update_components(array $options, array $langcodes = array(), array $components = array()) { - $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); + $langcodes = $langcodes ? $langcodes : array_keys(\Drupal::languageManager()->getLanguages()); if ($langcodes && $names = \Drupal\locale\Locale::config()->getComponentNames($components)) { return locale_config_batch_build($names, $langcodes, $options); } @@ -606,7 +606,7 @@ function locale_config_batch_refresh_name(array $names, array $langcodes, array if (!isset($context['result']['stats']['config'])) { $context['result']['stats']['config'] = 0; } - $context['result']['stats']['config'] += locale_config_update_multiple($names, $langcodes); + $context['result']['stats']['config'] += Locale::config()->updateConfigTranslations($names, $langcodes); foreach ($names as $name) { $context['result']['names'][] = $name; } @@ -639,41 +639,3 @@ function locale_config_batch_finished($success, array $results) { } } } - -/** - * Updates all configuration for names / languages. - * - * @param array $names - * Array of names of configuration objects to update. - * @param array $langcodes - * (optional) Array of language codes to update. Defaults to all languages. - * - * @return int - * Number of configuration objects retranslated. - */ -function locale_config_update_multiple(array $names, array $langcodes = array()) { - /** @var \Drupal\language\ConfigurableLanguageManagerInterface $language_manager */ - $language_manager = \Drupal::languageManager(); - $locale_config_manager = Locale::config(); - - $langcodes = $langcodes ? $langcodes : array_keys(locale_translatable_language_list()); - $count = 0; - foreach ($names as $name) { - $wrapper = $locale_config_manager->get($name); - foreach ($langcodes as $langcode) { - $translation = $wrapper->getValue() ? $wrapper->getTranslation($langcode)->getValue() : NULL; - if ($translation) { - $locale_config_manager->saveTranslationData($name, $langcode, $translation); - $count++; - } - else { - // 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); - } - } - } - } - return $count; -} diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module index 971b47d..a109ac4 100644 --- a/core/modules/locale/locale.module +++ b/core/modules/locale/locale.module @@ -24,6 +24,7 @@ use Drupal\Core\Language\LanguageInterface; use Drupal\language\ConfigurableLanguageInterface; use Drupal\Component\Utility\Crypt; +use Drupal\locale\Locale; /** * Regular expression pattern used to localize JavaScript strings. @@ -226,7 +227,7 @@ function locale_configurable_language_delete(ConfigurableLanguageInterface $lang locale_translate_delete_translation_files(array(), array($language->id())); // Remove translated configuration objects. - \Drupal\locale\Locale::config()->deleteLanguageTranslations($language->id()); + Locale::config()->deleteLanguageTranslations($language->id()); // Changing the language settings impacts the interface: _locale_invalidate_js($language->id()); @@ -313,6 +314,7 @@ function locale_get_plural($count, $langcode = NULL) { */ function locale_modules_installed($modules) { $components['module'] = $modules; + locale_system_set_config_langcodes($components); locale_system_update($components); } @@ -329,6 +331,7 @@ function locale_module_preuninstall($module) { */ function locale_themes_installed($themes) { $components['theme'] = $themes; + locale_system_set_config_langcodes($components); locale_system_update($components); } @@ -356,6 +359,41 @@ function locale_cron() { } /** + * Update default configuration when new modules or themes are installed. + * + * @param array $components + * An array of arrays of component (theme and/or module) names to import + * translations for, indexed by type. + */ +function locale_system_set_config_langcodes(array $components) { + // Need to rewrite some default configuration language codes if the default + // site language is not English. + $default_langcode = \Drupal::languageManager()->getDefaultLanguage()->getId(); + if ($default_langcode != 'en') { + // If just installed the locale module, we need to update all prior + // shipped configuration to the foreign site language. Otherwise keep the + // components list received to just update the shipped configuration just + // imported. + if (isset($components['module']) && in_array('locale', $components['module'])) { + $components = array(); + } + $names = Locale::config()->getComponentNames($components); + + foreach ($names as $name) { + $config = \Drupal::configFactory()->getEditable($name); + // Should only update if still exists in active configuration. If locale + // module is enabled later, then some configuration may not exist anymore. + if (!$config->isNew()) { + $langcode = $config->get('langcode'); + if (empty($langcode) || $langcode == 'en') { + $config->set('langcode', $default_langcode)->save(); + } + } + } + } +} + +/** * Imports translations when new modules or themes are installed. * * This function will start a batch to import translations for the added @@ -411,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. - \Drupal\locale\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 @@ -1020,19 +1056,14 @@ function _locale_refresh_translations($langcodes, $lids = array()) { /** * Refreshes configuration after string translations have been updated. * - * The information that will be refreshed includes: - * - JavaScript translations. - * - Locale cache. - * * @param array $langcodes * Language codes for updated translations. * @param array $lids * List of string identifiers that have been updated / created. */ function _locale_refresh_configuration(array $langcodes, array $lids) { - if ($lids && $langcodes && $names = \Drupal\locale\Locale::config()->getStringNames($lids)) { - \Drupal::moduleHandler()->loadInclude('locale', 'bulk.inc'); - locale_config_update_multiple($names, $langcodes); + if ($lids && $langcodes && $names = Locale::config()->getStringNames($lids)) { + Locale::config()->updateConfigTranslations($names, $langcodes); } } diff --git a/core/modules/locale/locale.services.yml b/core/modules/locale/locale.services.yml index 27cef2c..95683b3 100644 --- a/core/modules/locale/locale.services.yml +++ b/core/modules/locale/locale.services.yml @@ -22,6 +22,6 @@ services: - { name: stream_wrapper, scheme: translations } locale.config_subscriber: class: Drupal\locale\LocaleConfigSubscriber - arguments: ['@locale.storage', '@config.factory', '@locale.config.typed'] + arguments: ['@locale.storage', '@config.factory', '@locale.config.typed', '@language_manager'] tags: - { name: event_subscriber } diff --git a/core/modules/locale/src/LocaleConfigManager.php b/core/modules/locale/src/LocaleConfigManager.php index aee2d59..1fa9e2f 100644 --- a/core/modules/locale/src/LocaleConfigManager.php +++ b/core/modules/locale/src/LocaleConfigManager.php @@ -7,33 +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\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\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; @@ -66,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. @@ -99,61 +126,113 @@ public function __construct(StorageInterface $config_storage, StorageInterface $ } /** - * Gets locale wrapper with typed configuration data. + * Gets array of translation wrappers for translatable configuration. * * @param string $name * Configuration object name. * - * @return \Drupal\locale\LocaleTypedConfig - * Locale-wrapped configuration element. + * @return array + * Array of translatable elements of the default configuration in $name. */ - public function get($name) { - // Read default and current configuration data. - $default = $this->installStorageRead($name); - $updated = $this->configStorage->read($name); - // We get only the data that didn't change from default. - $data = $this->compareConfigData($default, $updated); - $definition = $this->typedConfigManager->getDefinition($name); - $data_definition = $this->typedConfigManager->buildDataDefinition($definition, $data); - // Unless the configuration has a explicit language code we assume English. - $langcode = isset($default['langcode']) ? $default['langcode'] : 'en'; - $wrapper = new LocaleTypedConfig($data_definition, $name, $langcode, $this, $this->typedConfigManager, $this->languageManager); - $wrapper->setValue($data); - return $wrapper; + public function getTranslatableDefaultConfig($name) { + if ($this->isSupported($name)) { + // Create typed configuration wrapper based on install storage data. + $data = $this->installStorageRead($name); + $type_definition = $this->typedConfigManager->getDefinition($name); + $data_definition = $this->typedConfigManager->buildDataDefinition($type_definition, $data); + $typed_config = $this->typedConfigManager->create($data_definition, $data); + return $this->getTranslatableData($typed_config); + } } /** - * Compares default configuration with updated data. + * Gets translatable configuration data for a typed configuration element. * - * @param array $default - * Default configuration data. - * @param array|false $updated - * Current configuration data, or FALSE if no configuration data existed. + * @param \Drupal\Core\TypedData\TypedDataInterface $element + * Typed configuration element. * - * @return array - * The elements of default configuration that haven't changed. + * @return array|\Drupal\Core\StringTranslation\TranslationWrapper + * Configuration data children of $element filtered to translatable children + * returned in a nested array modeled after the structure of the + * configuration. The leaf elements are TranslationWrapper instances to + * translate later as needed. */ - protected function compareConfigData(array $default, $updated) { - // Speed up comparison, specially for install operations. - if ($default === $updated) { - return $default; + protected function getTranslatableData(TypedDataInterface $element) { + $translatable = array(); + if ($element instanceof TraversableTypedDataInterface) { + foreach ($element as $key => $property) { + $value = $this->getTranslatableData($property); + if (!empty($value)) { + $translatable[$key] = $value; + } + } } - $result = array(); - foreach ($default as $key => $value) { - if (isset($updated[$key])) { - if (is_array($value)) { - $result[$key] = $this->compareConfigData($value, $updated[$key]); + else { + $definition = $element->getDataDefinition(); + if (!empty($definition['translatable'])) { + $options = array(); + if (isset($definition['translation context'])) { + $options['context'] = $definition['translation context']; + } + return new TranslationWrapper($element->getValue(), array(), $options); + } + } + return $translatable; + } + + /** + * Process the translatable data array with a given language. + * + * If the given language is translatable, will return the translated copy + * which will only contain strings that had translations. If the given + * language is English and is not translatable, will return a simplified + * array of the English source strings only. + * + * @param string $name + * The configuration name. + * @param array $active + * The active configuration data. + * @param array $translatable + * The translatable array structure, see this::getTranslatableData(). + * @param string $langcode + * The language code to process the array with + * + * @return array + * Processed translatable data array. Will only contain translations + * different from source strings or in case of untranslatable English, the + * source strings themselves. + */ + protected function processTranslatableData($name, array $active, array $translatable, $langcode) { + $translated = array(); + foreach ($translatable as $key => $item) { + if (!isset($active[$key])) { + continue; + } + if (is_array($item)) { + // Only add this key if there was a translated value underneath. + $value = $this->processTranslatableData($name, $active[$key], $item, $langcode); + if (!empty($value)) { + $translated[$key] = $value; + } + } + else { + /** @var \Drupal\Core\StringTranslation\TranslationWrapper $item */ + if ($langcode != 'en' || locale_translate_english()) { + $value = $this->translateString($name, $langcode, $item->getString(), $item->getOption('context')); } - elseif ($value === $updated[$key]) { - $result[$key] = $value; + else { + $value = $item->getString(); + } + if (!empty($value)) { + $translated[$key] = $value; } } } - return $result; + return $translated; } /** - * Saves translated configuration data. + * Saves translated configuration override. * * @param string $name * Configuration object name. @@ -162,10 +241,24 @@ protected function compareConfigData(array $default, $updated) { * @param array $data * Configuration data to be saved, that will be only the translated values. */ - public function saveTranslationData($name, $langcode, array $data) { - $this->isUpdating = TRUE; + public function saveTranslationOverride($name, $langcode, array $data) { + $this->isUpdatingFromLocale = TRUE; $this->languageManager->getLanguageConfigOverride($langcode, $name)->setData($data)->save(); - $this->isUpdating = FALSE; + $this->isUpdatingFromLocale = FALSE; + } + + /** + * Saves translated configuration data. + * + * @param string $name + * Configuration object name. + * @param array $data + * Configuration data to be saved with translations merged in. + */ + public function saveTranslationActive($name, array $data) { + $this->isUpdatingFromLocale = TRUE; + $this->configFactory->getEditable($name)->setData($data)->save(); + $this->isUpdatingFromLocale = FALSE; } /** @@ -176,10 +269,10 @@ public function saveTranslationData($name, $langcode, array $data) { * @param string $langcode * Language code. */ - public function deleteTranslationData($name, $langcode) { - $this->isUpdating = TRUE; + public function deleteTranslationOverride($name, $langcode) { + $this->isUpdatingFromLocale = TRUE; $this->languageManager->getLanguageConfigOverride($langcode, $name)->delete(); - $this->isUpdating = FALSE; + $this->isUpdatingFromLocale = FALSE; } /** @@ -192,7 +285,7 @@ public function deleteTranslationData($name, $langcode) { * @return array * Array of configuration object names. */ - public function getComponentNames(array $components) { + public function getComponentNames(array $components = array()) { $components = array_filter($components); if ($components) { $names = array(); @@ -209,27 +302,6 @@ public function getComponentNames(array $components) { } /** - * 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->deleteTranslationData($name, $langcode); - } - } - } - $this->isUpdating = FALSE; - } - - /** * Gets configuration names associated with strings. * * @param array $lids @@ -254,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; } /** @@ -334,25 +406,158 @@ public function translateString($name, $langcode, $source, $context) { * * @param string $name * Configuration name. - * @param \Drupal\Core\Language\LanguageInterface $language - * A language object. + * @param string $langcode + * A language code. * * @return bool * A boolean indicating if a language has configuration translations. */ - public function hasTranslation($name, LanguageInterface $language) { - $translation = $this->languageManager->getLanguageConfigOverride($language->getId(), $name); + public function hasTranslation($name, $langcode) { + $translation = $this->languageManager->getLanguageConfigOverride($langcode, $name); return !$translation->isNew(); } /** + * Returns the original language code for this shipped configuration. + * + * @param $name + * The configuration name. + * + * @return null|string + * Language code of the original shipped configuration. NULL if no such + * default configuration. + */ + public function defaultConfigLangcode($name) { + $shipped = $this->installStorageRead($name); + if (!empty($shipped)) { + return !empty($shipped['langcode']) ? $shipped['langcode'] : 'en'; + } + } + + /** + * Returns the current language code for this active configuration. + * + * @param $name + * The configuration name. + * + * @return null|string + * Language code of the current active configuration. NULL if no such active + * configuration. + */ + public function activeConfigLangcode($name) { + $active = $this->configStorage->read($name); + if (!empty($active)) { + return !empty($active['langcode']) ? $active['langcode'] : 'en'; + } + } + + /** + * Whether the given configuration is supported for interface translation. + * + * @param $name + * The configuration name. + * + * @return bool + * TRUE if interface translation is supported. + */ + public function isSupported($name) { + return $this->defaultConfigLangcode($name) == 'en' && !is_null($this->activeConfigLangcode($name)); + } + + /** * 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 isUpdatingTranslationsFromLocale() { + return $this->isUpdatingFromLocale; + } + + /** + * Updates all configuration translations for the names / languages provided. + * + * To be used when interface translation changes result in the need to update + * configuration translations to keep them in sync. + * + * @param array $names + * Array of names of configuration objects to update. + * @param array $langcodes + * (optional) Array of language codes to update. Defaults to all + * configurable languages. + * + * @return int + * Number of configuration objects updated (saved). */ - public function isUpdatingConfigTranslations() { - return $this->isUpdating; + public function updateConfigTranslations(array $names, array $langcodes = array()) { + $langcodes = $langcodes ? $langcodes : array_keys($this->languageManager->getLanguages()); + $count = 0; + foreach ($names as $name) { + // Only deal with configuration which was shipped. + if (!$this->isSupported($name)) { + continue; + } + + $translatable = $this->getTranslatableDefaultConfig($name); + $active_langcode = $this->activeConfigLangcode($name); + $active = $this->configStorage->read($name); + + foreach ($langcodes as $langcode) { + $processed = $this->processTranslatableData($name, $active, $translatable, $langcode); + if (empty($processed)) { + continue; + } + if ($langcode != $active_langcode) { + // If the language code is not the same as the active storage + // language, we should update a configuration override. + if (!empty($processed)) { + // Update translation data in configuration override. + $this->saveTranslationOverride($name, $langcode, $processed); + $count++; + } + elseif (!$this->languageManager->getLanguageConfigOverride($langcode, $name)->isNew()) { + // Delete language override if override exists. + $this->deleteTranslationOverride($name, $langcode); + } + } + elseif ($langcode != 'en' || locale_translate_english()) { + // If the language code is the active storage language, we should + // update. If it is English, we should only update if English is also + // translatable. + $active = $this->mergeToActiveConfig($active, $processed); + $this->saveTranslationActive($name, $active); + } + } + } + return $count; + } + + /** + * Merge translation data to the active configuration data. + * + * @param array $active + * Active configuration array. + * @param array $translated + * Translated configuration array. + * @return array + * The merged array. Only items from $active will be present merged with + * alternate value items from $translated. + */ + protected function mergeToActiveConfig(array $active, array $translated) { + foreach ($translated as $key => $item) { + if (isset($active[$key])) { + if (is_array($item)) { + $active[$key] = $this->mergeToActiveConfig($active[$key], $translated[$key]); + } + else { + $active[$key] = $item; + } + } + } + return $active; } /** diff --git a/core/modules/locale/src/LocaleConfigSubscriber.php b/core/modules/locale/src/LocaleConfigSubscriber.php index 6c42d50..13ed0f8 100644 --- a/core/modules/locale/src/LocaleConfigSubscriber.php +++ b/core/modules/locale/src/LocaleConfigSubscriber.php @@ -6,33 +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\TypedData\TraversableTypedDataInterface; -use Drupal\language\Config\LanguageConfigOverride; +use Drupal\Core\Config\StorableConfigBase; +use Drupal\Core\Language\LanguageManagerInterface; 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. @@ -49,6 +61,13 @@ class LocaleConfigSubscriber implements EventSubscriberInterface { protected $localeConfigManager; /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + + /** * Constructs a LocaleConfigSubscriber. * * @param \Drupal\locale\StringStorageInterface $string_storage @@ -58,134 +77,91 @@ class LocaleConfigSubscriber implements EventSubscriberInterface { * @param \Drupal\locale\LocaleConfigManager $locale_config_manager * The typed configuration manager. */ - public function __construct(StringStorageInterface $string_storage, ConfigFactoryInterface $config_factory, LocaleConfigManager $locale_config_manager) { - $this->stringStorage = $string_storage; + public function __construct(StringStorageInterface $string_storage, ConfigFactoryInterface $config_factory, LocaleConfigManager $locale_config_manager, LanguageManagerInterface $language_manager) { + $this->localeStorage = $string_storage; $this->configFactory = $config_factory; $this->localeConfigManager = $locale_config_manager; + $this->languageManager = $language_manager; } /** * {@inheritdoc} */ public static function getSubscribedEvents() { - $events[LanguageConfigOverrideEvents::SAVE_OVERRIDE] = 'onSave'; - $events[LanguageConfigOverrideEvents::DELETE_OVERRIDE] = 'onDelete'; + $events[LanguageConfigOverrideEvents::SAVE_OVERRIDE] = 'onOverrideSave'; + $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 onSave(LanguageConfigOverrideCrudEvent $event) { - // Do not mark strings as customized when community translations are being - // imported. - if ($this->localeConfigManager->isUpdatingConfigTranslations()) { - $callable = [$this, 'saveTranslation']; + 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); } - else { - $callable = [$this, 'saveCustomizedTranslation']; - } - - $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 onDelete(LanguageConfigOverrideCrudEvent $event) { - if ($this->localeConfigManager->isUpdatingConfigTranslations()) { - $callable = [$this, 'deleteTranslation']; + 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); } - else { - // Do not delete the string, but save a customized translation with the - // source value so that the deletion will not be reverted by importing - // community translations. - // @see \Drupal\locale\LocaleConfigSubscriber::saveCustomizedTranslation() - $callable = [$this, 'saveCustomizedTranslation']; - } - - $this->updateTranslationStrings($event, $callable); } /** - * Updates the translation strings of shipped configuration. + * 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) { - $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->get($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); } } } @@ -193,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::onDelete() */ - protected function saveCustomizedTranslation($source_value, $langcode, $translation_value = NULL) { - if ($translation = $this->getTranslation($source_value, $langcode, TRUE)) { - if (!isset($translation_value)) { - $translation_value = $source_value; - } - if ($translation->isNew() || $translation->getString() != $translation_value) { - $translation - ->setString($translation_value) + 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::onDelete() - */ - protected function deleteTranslation($source_value, $langcode) { - if ($translation = $this->getTranslation($source_value, $langcode, FALSE)) { - $translation->delete(); - } - } - - /** - * Gets a translation string. - * - * @param string $source_value - * The source string value. - * @param string $langcode - * The language code of the translation. - * @param bool $create_fallback - * (optional) By default if a source string could be found and no - * translation in the given language exists yet, a translation object is - * created. This can be circumvented by passing FALSE. - * - * @return \Drupal\locale\TranslationString|null - * The translation string if one was found or created. - */ - protected function getTranslation($source_value, $langcode, $create_fallback = TRUE) { - // There is no point in creating a translation without a source. - if ($source_string = $this->stringStorage->findString(['source' => $source_value])) { - // Get the translation for this original source string from locale. - $conditions = [ - 'lid' => $source_string->lid, - 'language' => $langcode, - ]; - $translations = $this->stringStorage->getTranslations($conditions + ['translated' => TRUE]); - if ($translations) { - return reset($translations); - } - elseif ($create_fallback) { - return $this->stringStorage->createTranslation($conditions); - } - } - } - } diff --git a/core/modules/locale/src/LocaleTypedConfig.php b/core/modules/locale/src/LocaleTypedConfig.php deleted file mode 100644 index ca31a38..0000000 --- a/core/modules/locale/src/LocaleTypedConfig.php +++ /dev/null @@ -1,209 +0,0 @@ -langcode = $langcode; - $this->localeConfig = $locale_config; - $this->typedConfigManager = $typed_config; - $this->languageManager = $language_manager; - } - - /** - * Gets wrapped typed config object. - */ - public function getTypedConfig() { - return $this->typedConfigManager->create($this->definition, $this->value); - } - - /** - * {@inheritdoc} - */ - public function getTranslation($langcode) { - $options = array( - 'source' => $this->langcode, - 'target' => $langcode, - ); - $data = $this->getElementTranslation($this->getTypedConfig(), $options); - return $this->typedConfigManager->create($this->definition, $data); - } - - /** - * {@inheritdoc} - */ - public function language() { - return $this->languageManager->getLanguage($this->langcode); - } - - /** - * Checks whether we can translate these languages. - * - * @param string $from_langcode - * Source language code. - * @param string $to_langcode - * Destination language code. - * - * @return bool - * TRUE if this translator supports translations for these languages. - */ - protected function canTranslate($from_langcode, $to_langcode) { - if ($from_langcode == 'en') { - return TRUE; - } - return FALSE; - } - - /** - * Gets translated configuration data for a typed configuration element. - * - * @param \Drupal\Core\TypedData\TypedDataInterface $element - * Typed configuration element. - * @param array $options - * Array with translation options that must contain the keys defined in - * \Drupal\locale\LocaleTypedConfig::translateElement(). - * - * @return array - * Configuration data translated to the requested language if available, - * an empty array otherwise. - */ - protected function getElementTranslation(TypedDataInterface $element, array $options) { - $translation = array(); - if ($element instanceof TraversableTypedDataInterface) { - $translation = $this->getArrayTranslation($element, $options); - } - elseif ($this->translateElement($element, $options)) { - $translation = $element->getValue(); - } - return $translation; - } - - /** - * Gets translated configuration data for a traversable element. - * - * @param \Drupal\Core\TypedData\TraversableTypedDataInterface $element - * Typed configuration array element. - * @param array $options - * Array with translation options that must contain the keys defined in - * \Drupal\locale\LocaleTypedConfig::translateElement(). - * - * @return array - * Configuration data translated to the requested language. - */ - protected function getArrayTranslation(TraversableTypedDataInterface $element, array $options) { - $translation = array(); - foreach ($element as $key => $property) { - $value = $this->getElementTranslation($property, $options); - if (!empty($value)) { - $translation[$key] = $value; - } - } - return $translation; - } - - /** - * Translates element's value if it fits our translation criteria. - * - * For an element to be translatable by locale module it needs to be of base - * type 'string' and have 'translatable = TRUE' in the element's definition. - * Translatable elements may use these additional keys in their data - * definition: - * - 'translatable', FALSE to opt out of translation. - * - 'translation context', to define the string context. - * - * @param \Drupal\Core\TypedData\TypedDataInterface $element - * Configuration element. - * @param array $options - * Array with translation options that must contain the following keys: - * - 'source', Source language code. - * - 'target', Target language code. - * - * @return bool - * Whether the element fits the translation criteria. - */ - protected function translateElement(TypedDataInterface $element, array $options) { - if ($this->canTranslate($options['source'], $options['target'])) { - $definition = $element->getDataDefinition(); - $value = $element->getValue(); - if ($value && !empty($definition['translatable'])) { - $context = isset($definition['translation context']) ? $definition['translation context'] : ''; - if ($translation = $this->localeConfig->translateString($this->name, $options['target'], $value, $context)) { - $element->setValue($translation); - return TRUE; - } - } - } - // The element does not have a translation. - return FALSE; - } - -} diff --git a/core/modules/locale/src/Tests/LocaleConfigSubscriberTest.php b/core/modules/locale/src/Tests/LocaleConfigSubscriberTest.php index f3f7415..6b2e27d 100644 --- a/core/modules/locale/src/Tests/LocaleConfigSubscriberTest.php +++ b/core/modules/locale/src/Tests/LocaleConfigSubscriberTest.php @@ -162,7 +162,7 @@ public function testLocaleDeleteTranslation() { */ protected function setUpNoTranslation($config_name, $key, $source) { // Add a source string with the configuration name as a location. This gets - // called from locale_config_update_multiple() normally. + // called from LocaleConfigManager::updateConfigTranslations() normally. $this->localeConfigManager->translateString($config_name, $this->langcode, $source, ''); $this->languageManager ->setConfigOverrideLanguage(ConfigurableLanguage::load($this->langcode)); @@ -192,8 +192,8 @@ protected function setUpNoTranslation($config_name, $key, $source) { protected function setUpTranslation($config_name, $key, $source, $translation) { // Create source and translation strings for the configuration value and add // the configuration name as a location. This would be performed by - // locale_translate_batch_import() and locale_config_update_multiple() - // normally. + // locale_translate_batch_import() and + // LocaleConfigManager::updateConfigTranslations() normally. $source_object = $this->stringStorage->createString([ 'source' => $source, 'context' => '', @@ -256,7 +256,7 @@ protected function saveLanguageOverride($config_name, $key, $value) { */ protected function saveLocaleTranslationData($config_name, $key, $value) { $this->localeConfigManager - ->saveTranslationData($config_name, $this->langcode, [$key => $value]); + ->saveTranslationOverride($config_name, $this->langcode, [$key => $value]); $this->configFactory->reset($config_name); $this->assertConfigValue($config_name, $key, $value); @@ -308,7 +308,7 @@ protected function deleteLanguageOverride($config_name, $key, $source_value) { * from the configuration factory after the deletion. */ protected function deleteLocaleTranslationData($config_name, $key, $source_value) { - $this->localeConfigManager->deleteTranslationData($config_name, $this->langcode); + $this->localeConfigManager->deleteTranslationOverride($config_name, $this->langcode); $this->configFactory->reset($config_name); $this->assertConfigValue($config_name, $key, $source_value); diff --git a/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php b/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php index 27f120d..afd6de9 100644 --- a/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php +++ b/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php @@ -79,7 +79,7 @@ public function testConfigTranslation() { ); $this->drupalPostForm('admin/config/regional/translate', $edit, t('Save translations')); - $wrapper = $this->container->get('locale.config.typed')->get('system.site'); + $wrapper = $this->container->get('locale.config.typed')->getTranslatableDefaultConfig('system.site'); // Get translation and check we've only got the site name. $translation = $wrapper->getTranslation($langcode); @@ -109,7 +109,7 @@ public function testConfigTranslation() { ); $this->drupalPostForm('admin/config/regional/translate', $edit, t('Save translations')); - $wrapper = $this->container->get('locale.config.typed')->get('core.date_format.medium'); + $wrapper = $this->container->get('locale.config.typed')->getTranslatableDefaultConfig('core.date_format.medium'); // Get translation and check we've only got the site name. $translation = $wrapper->getTranslation($langcode); @@ -162,7 +162,7 @@ public function testConfigTranslation() { $this->assertTrue(count($translations) == 1 && $translation->source == $string->source && $translation->translation == $image_style_label, 'Got only one translation for image configuration.'); // Try more complex configuration data. - $wrapper = $this->container->get('locale.config.typed')->get('image.style.medium'); + $wrapper = $this->container->get('locale.config.typed')->getTranslatableDefaultConfig('image.style.medium'); $translation = $wrapper->getTranslation($langcode); $property = $translation->get('label');