LocaleConfigSubscriber.php 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. <?php
  2. namespace Drupal\locale;
  3. use Drupal\Core\Config\ConfigCrudEvent;
  4. use Drupal\Core\Config\ConfigEvents;
  5. use Drupal\Core\Config\ConfigFactoryInterface;
  6. use Drupal\Core\Config\StorableConfigBase;
  7. use Drupal\Core\Installer\InstallerKernel;
  8. use Drupal\language\Config\LanguageConfigOverrideCrudEvent;
  9. use Drupal\language\Config\LanguageConfigOverrideEvents;
  10. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  11. /**
  12. * Updates strings translation when configuration translations change.
  13. *
  14. * This reacts to the updates of translated active configuration and
  15. * configuration language overrides. When those updates involve configuration
  16. * which was available as default configuration, we need to feed back changes
  17. * to any item which was originally part of that configuration to the interface
  18. * translation storage. Those updated translations are saved as customized, so
  19. * further community translation updates will not undo user changes.
  20. *
  21. * This subscriber does not respond to deleting active configuration or deleting
  22. * configuration translations. The locale storage is additive and we cannot be
  23. * sure that only a given configuration translation used a source string. So
  24. * we should not remove the translations from locale storage in these cases. The
  25. * configuration or override would itself be deleted either way.
  26. *
  27. * By design locale module only deals with sources in English.
  28. *
  29. * @see \Drupal\locale\LocaleConfigManager
  30. */
  31. class LocaleConfigSubscriber implements EventSubscriberInterface {
  32. /**
  33. * The configuration factory.
  34. *
  35. * @var \Drupal\Core\Config\ConfigFactoryInterface
  36. */
  37. protected $configFactory;
  38. /**
  39. * The typed configuration manager.
  40. *
  41. * @var \Drupal\locale\LocaleConfigManager
  42. */
  43. protected $localeConfigManager;
  44. /**
  45. * Constructs a LocaleConfigSubscriber.
  46. *
  47. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
  48. * The configuration factory.
  49. * @param \Drupal\locale\LocaleConfigManager $locale_config_manager
  50. * The typed configuration manager.
  51. */
  52. public function __construct(ConfigFactoryInterface $config_factory, LocaleConfigManager $locale_config_manager) {
  53. $this->configFactory = $config_factory;
  54. $this->localeConfigManager = $locale_config_manager;
  55. }
  56. /**
  57. * {@inheritdoc}
  58. */
  59. public static function getSubscribedEvents() {
  60. $events[LanguageConfigOverrideEvents::SAVE_OVERRIDE] = 'onOverrideChange';
  61. $events[LanguageConfigOverrideEvents::DELETE_OVERRIDE] = 'onOverrideChange';
  62. $events[ConfigEvents::SAVE] = 'onConfigSave';
  63. return $events;
  64. }
  65. /**
  66. * Updates the locale strings when a translated active configuration is saved.
  67. *
  68. * @param \Drupal\Core\Config\ConfigCrudEvent $event
  69. * The configuration event.
  70. */
  71. public function onConfigSave(ConfigCrudEvent $event) {
  72. // Only attempt to feed back configuration translation changes to locale if
  73. // the update itself was not initiated by locale data changes.
  74. if (!InstallerKernel::installationAttempted() && !$this->localeConfigManager->isUpdatingTranslationsFromLocale()) {
  75. $config = $event->getConfig();
  76. $langcode = $config->get('langcode') ?: 'en';
  77. $this->updateLocaleStorage($config, $langcode);
  78. }
  79. }
  80. /**
  81. * Updates the locale strings when a configuration override is saved/deleted.
  82. *
  83. * @param \Drupal\language\Config\LanguageConfigOverrideCrudEvent $event
  84. * The language configuration event.
  85. */
  86. public function onOverrideChange(LanguageConfigOverrideCrudEvent $event) {
  87. // Only attempt to feed back configuration override changes to locale if
  88. // the update itself was not initiated by locale data changes.
  89. if (!InstallerKernel::installationAttempted() && !$this->localeConfigManager->isUpdatingTranslationsFromLocale()) {
  90. $translation_config = $event->getLanguageConfigOverride();
  91. $langcode = $translation_config->getLangcode();
  92. $reference_config = $this->configFactory->getEditable($translation_config->getName())->get();
  93. $this->updateLocaleStorage($translation_config, $langcode, $reference_config);
  94. }
  95. }
  96. /**
  97. * Update locale storage based on configuration translations.
  98. *
  99. * @param \Drupal\Core\Config\StorableConfigBase $config
  100. * Active configuration or configuration translation override.
  101. * @param string $langcode
  102. * The language code of $config.
  103. * @param array $reference_config
  104. * (Optional) Reference configuration to check against if $config was an
  105. * override. This allows us to update locale keys for data not in the
  106. * override but still in the active configuration.
  107. */
  108. protected function updateLocaleStorage(StorableConfigBase $config, $langcode, array $reference_config = []) {
  109. $name = $config->getName();
  110. if ($this->localeConfigManager->isSupported($name) && locale_is_translatable($langcode)) {
  111. $translatables = $this->localeConfigManager->getTranslatableDefaultConfig($name);
  112. $this->processTranslatableData($name, $config->get(), $translatables, $langcode, $reference_config);
  113. }
  114. }
  115. /**
  116. * Process the translatable data array with a given language.
  117. *
  118. * @param string $name
  119. * The configuration name.
  120. * @param array $config
  121. * The active configuration data or override data.
  122. * @param array|\Drupal\Core\StringTranslation\TranslatableMarkup[] $translatable
  123. * The translatable array structure.
  124. * @see \Drupal\locale\LocaleConfigManager::getTranslatableData()
  125. * @param string $langcode
  126. * The language code to process the array with.
  127. * @param array $reference_config
  128. * (Optional) Reference configuration to check against if $config was an
  129. * override. This allows us to update locale keys for data not in the
  130. * override but still in the active configuration.
  131. */
  132. protected function processTranslatableData($name, array $config, array $translatable, $langcode, array $reference_config = []) {
  133. foreach ($translatable as $key => $item) {
  134. if (!isset($config[$key])) {
  135. if (isset($reference_config[$key])) {
  136. $this->resetExistingTranslations($name, $translatable[$key], $reference_config[$key], $langcode);
  137. }
  138. continue;
  139. }
  140. if (is_array($item)) {
  141. $reference_config_item = isset($reference_config[$key]) ? $reference_config[$key] : [];
  142. $this->processTranslatableData($name, $config[$key], $item, $langcode, $reference_config_item);
  143. }
  144. else {
  145. $this->saveCustomizedTranslation($name, $item->getUntranslatedString(), $item->getOption('context'), $config[$key], $langcode);
  146. }
  147. }
  148. }
  149. /**
  150. * Reset existing locale translations to their source values.
  151. *
  152. * Goes through $translatable to reset any existing translations to the source
  153. * string, so prior translations would not reappear in the configuration.
  154. *
  155. * @param string $name
  156. * The configuration name.
  157. * @param array|\Drupal\Core\StringTranslation\TranslatableMarkup $translatable
  158. * Either a possibly nested array with TranslatableMarkup objects at the
  159. * leaf items or a TranslatableMarkup object directly.
  160. * @param array|string $reference_config
  161. * Either a possibly nested array with strings at the leaf items or a string
  162. * directly. Only those $translatable items that are also present in
  163. * $reference_config will get translations reset.
  164. * @param string $langcode
  165. * The language code of the translation being processed.
  166. */
  167. protected function resetExistingTranslations($name, $translatable, $reference_config, $langcode) {
  168. if (is_array($translatable)) {
  169. foreach ($translatable as $key => $item) {
  170. if (isset($reference_config[$key])) {
  171. // Process further if the key still exists in the reference active
  172. // configuration and the default translation but not the current
  173. // configuration override.
  174. $this->resetExistingTranslations($name, $item, $reference_config[$key], $langcode);
  175. }
  176. }
  177. }
  178. elseif (!is_array($reference_config)) {
  179. $this->saveCustomizedTranslation($name, $translatable->getUntranslatedString(), $translatable->getOption('context'), $reference_config, $langcode);
  180. }
  181. }
  182. /**
  183. * Saves a translation string and marks it as customized.
  184. *
  185. * @param string $name
  186. * The configuration name.
  187. * @param string $source
  188. * The source string value.
  189. * @param string $context
  190. * The source string context.
  191. * @param string $new_translation
  192. * The translation string.
  193. * @param string $langcode
  194. * The language code of the translation.
  195. */
  196. protected function saveCustomizedTranslation($name, $source, $context, $new_translation, $langcode) {
  197. $locale_translation = $this->localeConfigManager->getStringTranslation($name, $langcode, $source, $context);
  198. if (!empty($locale_translation)) {
  199. // Save this translation as custom if it was a new translation and not the
  200. // same as the source. (The interface prefills translation values with the
  201. // source). Or if there was an existing (non-empty) translation and the
  202. // user changed it (even if it was changed back to the original value).
  203. // Otherwise the translation file would be overwritten with the locale
  204. // copy again later.
  205. $existing_translation = $locale_translation->getString();
  206. if (($locale_translation->isNew() && $source != $new_translation) ||
  207. (!$locale_translation->isNew() && ((empty($existing_translation) && $source != $new_translation) || ((!empty($existing_translation) && $new_translation != $existing_translation))))) {
  208. $locale_translation
  209. ->setString($new_translation)
  210. ->setCustomized(TRUE)
  211. ->save();
  212. }
  213. }
  214. }
  215. }