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