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