1<?php
2
3namespace Drupal\Tests\locale\Kernel;
4
5use Drupal\language\Entity\ConfigurableLanguage;
6use Drupal\locale\Locale;
7use Drupal\locale\StringInterface;
8use Drupal\locale\TranslationString;
9use Drupal\KernelTests\KernelTestBase;
10
11/**
12 * Tests that shipped configuration translations are updated correctly.
13 *
14 * @group locale
15 */
16class LocaleConfigSubscriberTest extends KernelTestBase {
17
18  /**
19   * {@inheritdoc}
20   */
21  protected static $modules = ['language', 'locale', 'system', 'locale_test'];
22
23  /**
24   * The configurable language manager used in this test.
25   *
26   * @var \Drupal\language\ConfigurableLanguageManagerInterface
27   */
28  protected $languageManager;
29
30  /**
31   * The configuration factory used in this test.
32   *
33   * @var \Drupal\Core\Config\ConfigFactoryInterface
34   */
35  protected $configFactory;
36
37  /**
38   * The string storage used in this test.
39   *
40   * @var \Drupal\locale\StringStorageInterface
41   */
42  protected $stringStorage;
43
44  /**
45   * The locale configuration manager used in this test.
46   *
47   * @var \Drupal\locale\LocaleConfigManager
48   */
49  protected $localeConfigManager;
50
51  /**
52   * {@inheritdoc}
53   */
54  protected function setUp(): void {
55    parent::setUp();
56
57    $this->setUpDefaultLanguage();
58
59    $this->installSchema('locale', ['locales_source', 'locales_target', 'locales_location']);
60
61    $this->setupLanguages();
62
63    $this->installConfig(['locale_test']);
64    // Simulate this hook invoked which would happen if in a non-kernel test
65    // or normal environment.
66    // @see locale_modules_installed()
67    // @see locale_system_update()
68    locale_system_set_config_langcodes();
69    $langcodes = array_keys(\Drupal::languageManager()->getLanguages());
70    $names = Locale::config()->getComponentNames();
71    Locale::config()->updateConfigTranslations($names, $langcodes);
72
73    $this->configFactory = $this->container->get('config.factory');
74    $this->stringStorage = $this->container->get('locale.storage');
75    $this->localeConfigManager = $this->container->get('locale.config_manager');
76    $this->languageManager = $this->container->get('language_manager');
77
78    $this->setUpLocale();
79  }
80
81  /**
82   * Sets up default language for this test.
83   */
84  protected function setUpDefaultLanguage() {
85    // Keep the default English.
86  }
87
88  /**
89   * Sets up languages needed for this test.
90   */
91  protected function setUpLanguages() {
92    ConfigurableLanguage::createFromLangcode('de')->save();
93  }
94
95  /**
96   * Sets up the locale storage strings to be in line with configuration.
97   */
98  protected function setUpLocale() {
99    // Set up the locale database the same way we have in the config samples.
100    $this->setUpNoTranslation('locale_test.no_translation', 'test', 'Test', 'de');
101    $this->setUpTranslation('locale_test.translation', 'test', 'English test', 'German test', 'de');
102    $this->setUpTranslation('locale_test.translation_multiple', 'test', 'English test', 'German test', 'de');
103  }
104
105  /**
106   * Tests creating translations of shipped configuration.
107   */
108  public function testCreateTranslation() {
109    $config_name = 'locale_test.no_translation';
110
111    $this->saveLanguageOverride($config_name, 'test', 'Test (German)', 'de');
112    $this->assertTranslation($config_name, 'Test (German)', 'de');
113  }
114
115  /**
116   * Tests creating translations configuration with multi value settings.
117   */
118  public function testCreateTranslationMultiValue() {
119    $config_name = 'locale_test.translation_multiple';
120
121    $this->saveLanguageOverride($config_name, 'test_multiple', ['string' => 'String (German)', 'another_string' => 'Another string (German)'], 'de');
122    $this->saveLanguageOverride($config_name, 'test_after_multiple', ['string' => 'After string (German)', 'another_string' => 'After another string (German)'], 'de');
123    $strings = $this->stringStorage->getTranslations([
124      'type' => 'configuration',
125      'name' => $config_name,
126      'language' => 'de',
127      'translated' => TRUE,
128    ]);
129    $this->assertCount(5, $strings);
130  }
131
132  /**
133   * Tests importing community translations of shipped configuration.
134   */
135  public function testLocaleCreateTranslation() {
136    $config_name = 'locale_test.no_translation';
137
138    $this->saveLocaleTranslationData($config_name, 'test', 'Test', 'Test (German)', 'de');
139    $this->assertTranslation($config_name, 'Test (German)', 'de', FALSE);
140  }
141
142  /**
143   * Tests updating translations of shipped configuration.
144   */
145  public function testUpdateTranslation() {
146    $config_name = 'locale_test.translation';
147
148    $this->saveLanguageOverride($config_name, 'test', 'Updated German test', 'de');
149    $this->assertTranslation($config_name, 'Updated German test', 'de');
150  }
151
152  /**
153   * Tests updating community translations of shipped configuration.
154   */
155  public function testLocaleUpdateTranslation() {
156    $config_name = 'locale_test.translation';
157
158    $this->saveLocaleTranslationData($config_name, 'test', 'English test', 'Updated German test', 'de');
159    $this->assertTranslation($config_name, 'Updated German test', 'de', FALSE);
160  }
161
162  /**
163   * Tests deleting translations of shipped configuration.
164   */
165  public function testDeleteTranslation() {
166    $config_name = 'locale_test.translation';
167
168    $this->deleteLanguageOverride($config_name, 'test', 'English test', 'de');
169    // Instead of deleting the translation, we need to keep a translation with
170    // the source value and mark it as customized to prevent the deletion being
171    // reverted by importing community translations.
172    $this->assertTranslation($config_name, 'English test', 'de');
173  }
174
175  /**
176   * Tests deleting community translations of shipped configuration.
177   */
178  public function testLocaleDeleteTranslation() {
179    $config_name = 'locale_test.translation';
180
181    $this->deleteLocaleTranslationData($config_name, 'test', 'English test', 'de');
182    $this->assertNoTranslation($config_name, 'de');
183  }
184
185  /**
186   * Sets up a configuration string without a translation.
187   *
188   * The actual configuration is already available by installing locale_test
189   * module, as it is done in LocaleConfigSubscriberTest::setUp(). This sets up
190   * the necessary source string and verifies that everything is as expected to
191   * avoid false positives.
192   *
193   * @param string $config_name
194   *   The configuration name.
195   * @param string $key
196   *   The configuration key.
197   * @param string $source
198   *   The source string.
199   * @param string $langcode
200   *   The language code.
201   */
202  protected function setUpNoTranslation($config_name, $key, $source, $langcode) {
203    $this->localeConfigManager->updateConfigTranslations([$config_name], [$langcode]);
204    $this->assertNoConfigOverride($config_name, $key, $source, $langcode);
205    $this->assertNoTranslation($config_name, $langcode);
206  }
207
208  /**
209   * Sets up a configuration string with a translation.
210   *
211   * The actual configuration is already available by installing locale_test
212   * module, as it is done in LocaleConfigSubscriberTest::setUp(). This sets up
213   * the necessary source and translation strings and verifies that everything
214   * is as expected to avoid false positives.
215   *
216   * @param string $config_name
217   *   The configuration name.
218   * @param string $key
219   *   The configuration key.
220   * @param string $source
221   *   The source string.
222   * @param string $translation
223   *   The translation string.
224   * @param string $langcode
225   *   The language code.
226   * @param bool $is_active
227   *   Whether the update will affect the active configuration.
228   */
229  protected function setUpTranslation($config_name, $key, $source, $translation, $langcode, $is_active = FALSE) {
230    // Create source and translation strings for the configuration value and add
231    // the configuration name as a location. This would be performed by
232    // locale_translate_batch_import() invoking
233    // LocaleConfigManager::updateConfigTranslations() normally.
234    $this->localeConfigManager->reset();
235    $this->localeConfigManager
236      ->getStringTranslation($config_name, $langcode, $source, '')
237      ->setString($translation)
238      ->setCustomized(FALSE)
239      ->save();
240    $this->configFactory->reset($config_name);
241    $this->localeConfigManager->reset();
242    $this->localeConfigManager->updateConfigTranslations([$config_name], [$langcode]);
243
244    if ($is_active) {
245      $this->assertActiveConfig($config_name, $key, $translation, $langcode);
246    }
247    else {
248      $this->assertConfigOverride($config_name, $key, $translation, $langcode);
249    }
250    $this->assertTranslation($config_name, $translation, $langcode, FALSE);
251  }
252
253  /**
254   * Saves a language override.
255   *
256   * This will invoke LocaleConfigSubscriber through the event dispatcher. To
257   * make sure the configuration was persisted correctly, the configuration
258   * value is checked. Because LocaleConfigSubscriber temporarily disables the
259   * override state of the configuration factory we check that the correct value
260   * is restored afterwards.
261   *
262   * @param string $config_name
263   *   The configuration name.
264   * @param string $key
265   *   The configuration key.
266   * @param string|array $value
267   *   The configuration value to save.
268   * @param string $langcode
269   *   The language code.
270   */
271  protected function saveLanguageOverride($config_name, $key, $value, $langcode) {
272    $translation_override = $this->languageManager
273      ->getLanguageConfigOverride($langcode, $config_name);
274    $translation_override
275      ->set($key, $value)
276      ->save();
277    $this->configFactory->reset($config_name);
278
279    $this->assertConfigOverride($config_name, $key, $value, $langcode);
280  }
281
282  /**
283   * Saves translation data from locale module.
284   *
285   * This will invoke LocaleConfigSubscriber through the event dispatcher. To
286   * make sure the configuration was persisted correctly, the configuration
287   * value is checked. Because LocaleConfigSubscriber temporarily disables the
288   * override state of the configuration factory we check that the correct value
289   * is restored afterwards.
290   *
291   * @param string $config_name
292   *   The configuration name.
293   * @param string $key
294   *   The configuration key.
295   * @param string $source
296   *   The source string.
297   * @param string $translation
298   *   The translation string to save.
299   * @param string $langcode
300   *   The language code.
301   * @param bool $is_active
302   *   Whether the update will affect the active configuration.
303   */
304  protected function saveLocaleTranslationData($config_name, $key, $source, $translation, $langcode, $is_active = FALSE) {
305    $this->localeConfigManager->reset();
306    $this->localeConfigManager
307      ->getStringTranslation($config_name, $langcode, $source, '')
308      ->setString($translation)
309      ->save();
310    $this->localeConfigManager->reset();
311    $this->localeConfigManager->updateConfigTranslations([$config_name], [$langcode]);
312    $this->configFactory->reset($config_name);
313
314    if ($is_active) {
315      $this->assertActiveConfig($config_name, $key, $translation, $langcode);
316    }
317    else {
318      $this->assertConfigOverride($config_name, $key, $translation, $langcode);
319    }
320  }
321
322  /**
323   * Deletes a language override.
324   *
325   * This will invoke LocaleConfigSubscriber through the event dispatcher. To
326   * make sure the configuration was persisted correctly, the configuration
327   * value is checked. Because LocaleConfigSubscriber temporarily disables the
328   * override state of the configuration factory we check that the correct value
329   * is restored afterwards.
330   *
331   * @param string $config_name
332   *   The configuration name.
333   * @param string $key
334   *   The configuration key.
335   * @param string $source_value
336   *   The source configuration value to verify the correct value is returned
337   *   from the configuration factory after the deletion.
338   * @param string $langcode
339   *   The language code.
340   */
341  protected function deleteLanguageOverride($config_name, $key, $source_value, $langcode) {
342    $translation_override = $this->languageManager
343      ->getLanguageConfigOverride($langcode, $config_name);
344    $translation_override
345      ->clear($key)
346      ->save();
347    $this->configFactory->reset($config_name);
348
349    $this->assertNoConfigOverride($config_name, $key, $source_value, $langcode);
350  }
351
352  /**
353   * Deletes translation data from locale module.
354   *
355   * This will invoke LocaleConfigSubscriber through the event dispatcher. To
356   * make sure the configuration was persisted correctly, the configuration
357   * value is checked. Because LocaleConfigSubscriber temporarily disables the
358   * override state of the configuration factory we check that the correct value
359   * is restored afterwards.
360   *
361   * @param string $config_name
362   *   The configuration name.
363   * @param string $key
364   *   The configuration key.
365   * @param string $source_value
366   *   The source configuration value to verify the correct value is returned
367   *   from the configuration factory after the deletion.
368   * @param string $langcode
369   *   The language code.
370   */
371  protected function deleteLocaleTranslationData($config_name, $key, $source_value, $langcode) {
372    $this->localeConfigManager
373      ->getStringTranslation($config_name, $langcode, $source_value, '')
374      ->delete();
375    $this->localeConfigManager->reset();
376    $this->localeConfigManager->updateConfigTranslations([$config_name], [$langcode]);
377    $this->configFactory->reset($config_name);
378
379    $this->assertNoConfigOverride($config_name, $key, $source_value, $langcode);
380  }
381
382  /**
383   * Ensures configuration override is not present anymore.
384   *
385   * @param string $config_name
386   *   The configuration name.
387   * @param string $langcode
388   *   The language code.
389   */
390  protected function assertNoConfigOverride($config_name, $langcode) {
391    $config_langcode = $this->configFactory->getEditable($config_name)->get('langcode');
392    $override = $this->languageManager->getLanguageConfigOverride($langcode, $config_name);
393    $this->assertNotEquals($langcode, $config_langcode);
394    $this->assertTrue($override->isNew());
395  }
396
397  /**
398   * Ensures configuration was saved correctly.
399   *
400   * @param string $config_name
401   *   The configuration name.
402   * @param string $key
403   *   The configuration key.
404   * @param string $value
405   *   The configuration value.
406   * @param string $langcode
407   *   The language code.
408   */
409  protected function assertConfigOverride($config_name, $key, $value, $langcode) {
410    $config_langcode = $this->configFactory->getEditable($config_name)->get('langcode');
411    $override = $this->languageManager->getLanguageConfigOverride($langcode, $config_name);
412    $this->assertNotEquals($langcode, $config_langcode);
413    $this->assertEquals($value, $override->get($key));
414  }
415
416  /**
417   * Ensures configuration was saved correctly.
418   *
419   * @param string $config_name
420   *   The configuration name.
421   * @param string $key
422   *   The configuration key.
423   * @param string $value
424   *   The configuration value.
425   * @param string $langcode
426   *   The language code.
427   */
428  protected function assertActiveConfig($config_name, $key, $value, $langcode) {
429    $config = $this->configFactory->getEditable($config_name);
430    $this->assertEquals($langcode, $config->get('langcode'));
431    $this->assertSame($value, $config->get($key));
432  }
433
434  /**
435   * Ensures no translation exists.
436   *
437   * @param string $config_name
438   *   The configuration name.
439   * @param string $langcode
440   *   The language code.
441   */
442  protected function assertNoTranslation($config_name, $langcode) {
443    $strings = $this->stringStorage->getTranslations([
444      'type' => 'configuration',
445      'name' => $config_name,
446      'language' => $langcode,
447      'translated' => TRUE,
448    ]);
449    $this->assertSame([], $strings);
450  }
451
452  /**
453   * Ensures a translation exists and is marked as customized.
454   *
455   * @param string $config_name
456   *   The configuration name.
457   * @param string|array $translation
458   *   The translation.
459   * @param string $langcode
460   *   The language code.
461   * @param bool $customized
462   *   Whether or not the string should be asserted to be customized or not
463   *   customized.
464   */
465  protected function assertTranslation($config_name, $translation, $langcode, $customized = TRUE) {
466    // Make sure a string exists.
467    $strings = $this->stringStorage->getTranslations([
468      'type' => 'configuration',
469      'name' => $config_name,
470      'language' => $langcode,
471      'translated' => TRUE,
472    ]);
473    $this->assertCount(1, $strings);
474    $string = reset($strings);
475    $this->assertInstanceOf(StringInterface::class, $string);
476    /** @var \Drupal\locale\StringInterface $string */
477    $this->assertSame($translation, $string->getString());
478    $this->assertTrue($string->isTranslation());
479    $this->assertInstanceOf(TranslationString::class, $string);
480    /** @var \Drupal\locale\TranslationString $string */
481    // Make sure the string is marked as customized so that it does not get
482    // overridden when the string translations are updated.
483    $this->assertEquals($customized, (bool) $string->customized);
484  }
485
486}
487