diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index 3047561ed69cbdf667f05e0901a7abaf6c1f74db..c7f806a053a189d21cdd7d8e41f10beaca6107ad 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -207,7 +207,15 @@ config_object: requiredKey: false type: _core_config_info langcode: + requiredKey: false type: langcode + constraints: + # The `langcode` key: + # - MUST be specified when there are translatable values + # - MUST NOT be specified when there are no translatable values. + # Translatable values are specified for this config schema type (a subtype of `type: config_object`) if the + # `translatable` flag is present and set to `true` for *any* element in that config schema type. + LangcodeRequiredIfTranslatableValues: ~ # Mail text with subject and body parts. mail: diff --git a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/LangcodeRequiredIfTranslatableValuesConstraint.php b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/LangcodeRequiredIfTranslatableValuesConstraint.php new file mode 100644 index 0000000000000000000000000000000000000000..41205ac57e5f9f4a47e599a734246a23918ae38f --- /dev/null +++ b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/LangcodeRequiredIfTranslatableValuesConstraint.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Config\Plugin\Validation\Constraint; + +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\Core\Validation\Attribute\Constraint; +use Symfony\Component\Validator\Constraint as SymfonyConstraint; + +#[Constraint( + id: 'LangcodeRequiredIfTranslatableValues', + label: new TranslatableMarkup('Translatable config has langcode', [], ['context' => 'Validation']), + type: ['config_object'] +)] +class LangcodeRequiredIfTranslatableValuesConstraint extends SymfonyConstraint { + + /** + * The error message if this config object is missing a `langcode`. + * + * @var string + */ + public string $missingMessage = "The @name config object must specify a language code, because it contains translatable values."; + + /** + * The error message if this config object contains a superfluous `langcode`. + * + * @var string + */ + public string $superfluousMessage = "The @name config object does not contain any translatable values, so it should not specify a language code."; + +} diff --git a/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/LangcodeRequiredIfTranslatableValuesConstraintValidator.php b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/LangcodeRequiredIfTranslatableValuesConstraintValidator.php new file mode 100644 index 0000000000000000000000000000000000000000..b6b3dbe9e07d170f0881e1bed0331f048e71d64d --- /dev/null +++ b/core/lib/Drupal/Core/Config/Plugin/Validation/Constraint/LangcodeRequiredIfTranslatableValuesConstraintValidator.php @@ -0,0 +1,47 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Config\Plugin\Validation\Constraint; + +use Drupal\Core\Config\Schema\Mapping; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\LogicException; + +/** + * Validates the LangcodeRequiredIfTranslatableValues constraint. + */ +final class LangcodeRequiredIfTranslatableValuesConstraintValidator extends ConstraintValidator { + + /** + * {@inheritdoc} + */ + public function validate(mixed $value, Constraint $constraint) { + assert($constraint instanceof LangcodeRequiredIfTranslatableValuesConstraint); + + $mapping = $this->context->getObject(); + assert($mapping instanceof Mapping); + if ($mapping !== $this->context->getRoot()) { + throw new LogicException('The LangcodeRequiredIfTranslatableValues constraint can only operate on the root object being validated.'); + } + + assert(in_array('langcode', $mapping->getValidKeys(), TRUE)); + + $is_translatable = $mapping->hasTranslatableElements(); + + if ($is_translatable && !array_key_exists('langcode', $value)) { + $this->context->buildViolation($constraint->missingMessage) + ->setParameter('@name', $mapping->getName()) + ->addViolation(); + return; + } + if (!$is_translatable && array_key_exists('langcode', $value)) { + // @todo Convert this deprecation to an actual validation error in + // https://www.drupal.org/project/drupal/issues/3440238. + // phpcs:ignore + @trigger_error(str_replace('@name', $mapping->getName(), $constraint->superfluousMessage), E_USER_DEPRECATED); + } + } + +} diff --git a/core/lib/Drupal/Core/Config/Schema/ArrayElement.php b/core/lib/Drupal/Core/Config/Schema/ArrayElement.php index 11fc9aca2414d733946e58cd37e50423ed602342..9eb649be771e4df2cb745a050143f246a8c20ab2 100644 --- a/core/lib/Drupal/Core/Config/Schema/ArrayElement.php +++ b/core/lib/Drupal/Core/Config/Schema/ArrayElement.php @@ -14,6 +14,25 @@ abstract class ArrayElement extends Element implements \IteratorAggregate, Typed */ protected $elements; + /** + * Determines if there is a translatable value. + * + * @return bool + * Returns true if a translatable element is found. + */ + public function hasTranslatableElements(): bool { + foreach ($this as $element) { + // Early return if found. + if ($element->getDataDefinition()['translatable'] === TRUE) { + return TRUE; + } + if ($element instanceof ArrayElement && $element->hasTranslatableElements()) { + return TRUE; + } + } + return FALSE; + } + /** * Gets valid configuration data keys. * diff --git a/core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php b/core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php index 3adb9075ad68013e30dcc373d93737fb26b04b59..b928df3eebe4ef2fe4ba1fda8885e69b5a23644d 100644 --- a/core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php +++ b/core/lib/Drupal/Core/Config/Schema/SchemaCheckTrait.php @@ -93,13 +93,6 @@ trait SchemaCheckTrait { * valid. */ public function checkConfigSchema(TypedConfigManagerInterface $typed_config, $config_name, $config_data, bool $validate_constraints = FALSE) { - // We'd like to verify that the top-level type is either config_base, - // config_entity, or a derivative. The only thing we can really test though - // is that the schema supports having langcode in it. So add 'langcode' to - // the data if it doesn't already exist. - if (!isset($config_data['langcode'])) { - $config_data['langcode'] = 'en'; - } $this->configName = $config_name; if (!$typed_config->hasConfigSchema($config_name)) { return FALSE; diff --git a/core/modules/config/tests/config_schema_test/config/install/config_schema_test.ignore.yml b/core/modules/config/tests/config_schema_test/config/install/config_schema_test.ignore.yml index 37e231ac0618f998d7ed4f9365a470123a255a15..a37159915e739213a0be8e17f59732e2e966e67d 100644 --- a/core/modules/config/tests/config_schema_test/config/install/config_schema_test.ignore.yml +++ b/core/modules/config/tests/config_schema_test/config/install/config_schema_test.ignore.yml @@ -1,3 +1,4 @@ +langcode: en label: 'Label string' irrelevant: 123 indescribable: diff --git a/core/modules/config/tests/config_schema_test/config/install/config_schema_test.some_schema.some_module.section_one.subsection.yml b/core/modules/config/tests/config_schema_test/config/install/config_schema_test.some_schema.some_module.section_one.subsection.yml index e6fae1ad0cbcde435182688cec65f635f4a7222d..a1c5f722fcd4853bee303856884aaabd35f9c8fe 100644 --- a/core/modules/config/tests/config_schema_test/config/install/config_schema_test.some_schema.some_module.section_one.subsection.yml +++ b/core/modules/config/tests/config_schema_test/config/install/config_schema_test.some_schema.some_module.section_one.subsection.yml @@ -1,2 +1,3 @@ +langcode: en test_id: 'Test id' test_description: 'Test description' diff --git a/core/modules/config/tests/config_schema_test/config/install/config_schema_test.some_schema.some_module.section_two.subsection.yml b/core/modules/config/tests/config_schema_test/config/install/config_schema_test.some_schema.some_module.section_two.subsection.yml index e6fae1ad0cbcde435182688cec65f635f4a7222d..a1c5f722fcd4853bee303856884aaabd35f9c8fe 100644 --- a/core/modules/config/tests/config_schema_test/config/install/config_schema_test.some_schema.some_module.section_two.subsection.yml +++ b/core/modules/config/tests/config_schema_test/config/install/config_schema_test.some_schema.some_module.section_two.subsection.yml @@ -1,2 +1,3 @@ +langcode: en test_id: 'Test id' test_description: 'Test description' diff --git a/core/modules/config/tests/config_test/config/install/config_test.no_status.default.yml b/core/modules/config/tests/config_test/config/install/config_test.no_status.default.yml index 3e50e3bbd3de64665a873daf098a59326e0cefba..8892305f39d687990b6ba0b765a12bb5ec61131d 100644 --- a/core/modules/config/tests/config_test/config/install/config_test.no_status.default.yml +++ b/core/modules/config/tests/config_test/config/install/config_test.no_status.default.yml @@ -1,2 +1,3 @@ +langcode: en id: default label: Default diff --git a/core/modules/config/tests/config_test/config/install/config_test.validation.yml b/core/modules/config/tests/config_test/config/install/config_test.validation.yml index 5d6d8686526e356169c91d9caa1f344256904e53..01d07b3793fa958ff0a204a956925fed40d09003 100644 --- a/core/modules/config/tests/config_test/config/install/config_test.validation.yml +++ b/core/modules/config/tests/config_test/config/install/config_test.validation.yml @@ -6,5 +6,4 @@ giraffe: hum1: hum1 hum2: hum2 uuid: '7C30C50E-641A-4E34-A7F1-46BCFB9BE5A3' -langcode: en string__not_blank: 'this is a label' diff --git a/core/modules/config/tests/config_test/config_test.module b/core/modules/config/tests/config_test/config_test.module index af5d99cbd28fc34f755551351fc76fad102e9961..b9fac8f793bc13cb758d4bceb812daa915f4b3c9 100644 --- a/core/modules/config/tests/config_test/config_test.module +++ b/core/modules/config/tests/config_test/config_test.module @@ -38,7 +38,7 @@ function config_test_entity_type_alter(array &$entity_types) { $config_test_no_status->set('id', 'config_test_no_status'); $config_test_no_status->set('entity_keys', $keys); $config_test_no_status->set('config_prefix', 'no_status'); - $config_test_no_status->set('mergedConfigExport', ['id' => 'id', 'label' => 'label', 'uuid' => 'uuid']); + $config_test_no_status->set('mergedConfigExport', ['id' => 'id', 'label' => 'label', 'uuid' => 'uuid', 'langcode' => 'langcode']); if (\Drupal::service('state')->get('config_test.lookup_keys', FALSE)) { $entity_types['config_test']->set('lookup_keys', ['uuid', 'style']); } diff --git a/core/modules/dblog/config/install/dblog.settings.yml b/core/modules/dblog/config/install/dblog.settings.yml index 138b3ea58be9e07ff274c877bdd91bbd084d083a..88add889ac50e964a7bcfb8ad068d9893c0339fe 100644 --- a/core/modules/dblog/config/install/dblog.settings.yml +++ b/core/modules/dblog/config/install/dblog.settings.yml @@ -1,2 +1 @@ -langcode: en row_limit: 1000 diff --git a/core/modules/dblog/dblog.post_update.php b/core/modules/dblog/dblog.post_update.php index cae2098378f9c263caafd508b1c5ac23860df376..9fa7a0c2fe6afc673e6c9cfaff19fab92ada566b 100644 --- a/core/modules/dblog/dblog.post_update.php +++ b/core/modules/dblog/dblog.post_update.php @@ -5,18 +5,6 @@ * Post update functions for the Database Logging module. */ -/** - * Ensures the `dblog.settings` config has a langcode. - */ -function dblog_post_update_add_langcode_to_settings(): void { - $config = \Drupal::configFactory()->getEditable('dblog.settings'); - if ($config->get('langcode')) { - return; - } - $config->set('langcode', \Drupal::languageManager()->getDefaultLanguage()->getId()) - ->save(); -} - /** * Implements hook_removed_post_updates(). */ diff --git a/core/modules/dblog/tests/src/Functional/UpdatePathTest.php b/core/modules/dblog/tests/src/Functional/UpdatePathTest.php index e6be5c9992667a876e4ecc3e529b1bab8aaeae09..00bf2b7d6826b4b6a154e7defb26814ba765e574 100644 --- a/core/modules/dblog/tests/src/Functional/UpdatePathTest.php +++ b/core/modules/dblog/tests/src/Functional/UpdatePathTest.php @@ -24,18 +24,6 @@ protected function setDatabaseDumpFiles() { ]; } - /** - * Tests that updating adds a langcode to the dblog.settings config. - */ - public function testAddLangcodeToSettings(): void { - $this->assertEmpty($this->config('dblog.settings')->get('langcode')); - $this->runUpdates(); - $default_langcode = $this->container->get('language_manager') - ->getDefaultLanguage() - ->getId(); - $this->assertSame($default_langcode, $this->config('dblog.settings')->get('langcode')); - } - /** * Tests that, after update 10101, the 'wid' column can be a 64-bit integer. */ diff --git a/core/modules/jsonapi/config/install/jsonapi.settings.yml b/core/modules/jsonapi/config/install/jsonapi.settings.yml index 6ec8f68a47930758d5ad615eefcc03d36a881dd1..dc0e229cb2def1a95873b63116eeb7cf8d4bf584 100644 --- a/core/modules/jsonapi/config/install/jsonapi.settings.yml +++ b/core/modules/jsonapi/config/install/jsonapi.settings.yml @@ -1,4 +1,3 @@ -langcode: en read_only: true maintenance_header_retry_seconds: min: 5 diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php index 631159a8b3efd1db1918469f739c330a27a28ce4..e9608bff6c529189ed2844e0ec989197088306da 100644 --- a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php +++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php @@ -703,6 +703,10 @@ public function testSimpleConfigBasedLayout() { // Prepare an object with a pre-existing section. $this->container->get('config.factory')->getEditable('layout_builder_test.test_simple_config.existing') ->set('sections', [(new Section('layout_twocol'))->toArray()]) + // `layout_builder_test.test_simple_config.existing.sections.0.layout_settings.label` + // contains a translatable label, so a `langcode` is required. + // @see \Drupal\Core\Config\Plugin\Validation\Constraint\LangcodeRequiredIfTranslatableValuesConstraint + ->set('langcode', 'en') ->save(); // The pre-existing section is found. diff --git a/core/modules/locale/src/LocaleConfigManager.php b/core/modules/locale/src/LocaleConfigManager.php index 9bde0a41009adcfffaf864b8a05204c56cbc2cd9..374bae750fdb89c24de5362b1fbd070ff28fec10 100644 --- a/core/modules/locale/src/LocaleConfigManager.php +++ b/core/modules/locale/src/LocaleConfigManager.php @@ -668,8 +668,12 @@ public function updateDefaultConfigLangcodes() { // 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()) { + $typed_config = $this->typedConfigManager->createFromNameAndData($config->getName(), $config->getRawData()); $langcode = $config->get('langcode'); - if (empty($langcode) || $langcode == 'en') { + // Only set a `langcode` if this config actually contains translatable + // data. + // @see \Drupal\Core\Config\Plugin\Validation\Constraint\LangcodeRequiredIfTranslatableValuesConstraint + if (!empty($this->getTranslatableData($typed_config)) && (empty($langcode) || $langcode == 'en')) { $config->set('langcode', $default_langcode)->save(); } } diff --git a/core/modules/locale/tests/modules/locale_test/config/install/locale_test.no_translation.yml b/core/modules/locale/tests/modules/locale_test/config/install/locale_test.no_translation.yml index 4583231dc91a000eaae03497f7b6017430a29168..9c26a378496c3862a705eaee38b9f0fdf338ce36 100644 --- a/core/modules/locale/tests/modules/locale_test/config/install/locale_test.no_translation.yml +++ b/core/modules/locale/tests/modules/locale_test/config/install/locale_test.no_translation.yml @@ -1 +1,2 @@ +langcode: en test: Test diff --git a/core/modules/locale/tests/modules/locale_test/config/install/locale_test.translation.yml b/core/modules/locale/tests/modules/locale_test/config/install/locale_test.translation.yml index 03d3b1cf9d5ee155ad73442263e9f7f2ea51ccf9..b54fc975d448dcf672434a4a042b55a96b28c348 100644 --- a/core/modules/locale/tests/modules/locale_test/config/install/locale_test.translation.yml +++ b/core/modules/locale/tests/modules/locale_test/config/install/locale_test.translation.yml @@ -1 +1,2 @@ +langcode: en test: English test diff --git a/core/modules/locale/tests/modules/locale_test/config/install/locale_test.translation_multiple.yml b/core/modules/locale/tests/modules/locale_test/config/install/locale_test.translation_multiple.yml index 1be6abaead9816566e3915b0dac584ad940b2dd8..8b898ac41cd2b0fe5ef8a4796ffa63454026c37e 100644 --- a/core/modules/locale/tests/modules/locale_test/config/install/locale_test.translation_multiple.yml +++ b/core/modules/locale/tests/modules/locale_test/config/install/locale_test.translation_multiple.yml @@ -1,3 +1,4 @@ +langcode: en test: English test test_multiple: string: 'A string' diff --git a/core/modules/locale/tests/modules/locale_test_translate/config/install/locale_test_translate.settings.yml b/core/modules/locale/tests/modules/locale_test_translate/config/install/locale_test_translate.settings.yml index 544dad7cddcf8d1e8bd43a970a771c2f6aeee871..00e9393beceb8640d83655c5286b65ed4954953f 100644 --- a/core/modules/locale/tests/modules/locale_test_translate/config/install/locale_test_translate.settings.yml +++ b/core/modules/locale/tests/modules/locale_test_translate/config/install/locale_test_translate.settings.yml @@ -1,3 +1,4 @@ +langcode: en translatable_no_default: '' translatable_default_with_translation: 'Locale can translate' translatable_default_with_no_translation: 'Locale can not translate' diff --git a/core/modules/migrate/src/Plugin/migrate/destination/Config.php b/core/modules/migrate/src/Plugin/migrate/destination/Config.php index ea560e1832224a8a8cce85b5dc2e63328c05a35f..bfa4da591967d3e8c54a76100b6656f2055c562f 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/Config.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/Config.php @@ -4,6 +4,7 @@ use Drupal\Component\Plugin\DependentPluginInterface; use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Config\TypedConfigManagerInterface; use Drupal\Core\Entity\DependencyTrait; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; @@ -85,6 +86,13 @@ class Config extends DestinationBase implements ContainerFactoryPluginInterface, */ protected $language_manager; + /** + * The typed config manager service. + * + * @var \Drupal\Core\Config\TypedConfigManagerInterface + */ + protected TypedConfigManagerInterface $typedConfigManager; + /** * Constructs a Config destination object. * @@ -100,14 +108,21 @@ class Config extends DestinationBase implements ContainerFactoryPluginInterface, * The configuration factory. * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager * The language manager. + * @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager + * The typed config manager. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, ConfigFactoryInterface $config_factory, LanguageManagerInterface $language_manager) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, ConfigFactoryInterface $config_factory, LanguageManagerInterface $language_manager, TypedConfigManagerInterface $typed_config_manager = NULL) { parent::__construct($configuration, $plugin_id, $plugin_definition, $migration); $this->config = $config_factory->getEditable($configuration['config_name']); $this->language_manager = $language_manager; if ($this->isTranslationDestination()) { $this->supportsRollback = TRUE; } + if ($typed_config_manager === NULL) { + @trigger_error('Calling ' . __METHOD__ . '() without the $typed_config_manager argument is deprecated in drupal:10.3.0 and is removed in drupal:11.0.0. See https://www.drupal.org/node/3440502', E_USER_DEPRECATED); + $typed_config_manager = \Drupal::service(TypedConfigManagerInterface::class); + } + $this->typedConfigManager = $typed_config_manager; } /** @@ -120,7 +135,8 @@ public static function create(ContainerInterface $container, array $configuratio $plugin_definition, $migration, $container->get('config.factory'), - $container->get('language_manager') + $container->get('language_manager'), + $container->get(TypedConfigManagerInterface::class) ); } @@ -137,6 +153,16 @@ public function import(Row $row, array $old_destination_id_values = []) { $this->config->set(str_replace(Row::PROPERTY_SEPARATOR, '.', $key), $value); } } + + $name = $this->config->getName(); + // Ensure that translatable config has `langcode` specified. + // @see \Drupal\Core\Config\Plugin\Validation\Constraint\LangcodeRequiredIfTranslatableValuesConstraint + if ($this->typedConfigManager->hasConfigSchema($name) + && $this->typedConfigManager->createFromNameAndData($name, $this->config->getRawData())->hasTranslatableElements() + && !$this->config->get('langcode') + ) { + $this->config->set('langcode', $this->language_manager->getDefaultLanguage()->getId()); + } $this->config->save(); $ids[] = $this->config->getName(); if ($this->isTranslationDestination()) { diff --git a/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/CheckRequirementsTest.php b/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/CheckRequirementsTest.php index b53f0004dfeec511a1ed4132ab4f68d6bac8d17b..ac5c1023c735d9074fd9e7c036383f45cca9c19f 100644 --- a/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/CheckRequirementsTest.php +++ b/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/CheckRequirementsTest.php @@ -5,6 +5,7 @@ namespace Drupal\Tests\migrate\Unit\Plugin\migrate\destination; use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Config\TypedConfigManagerInterface; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Tests\UnitTestCase; use Drupal\migrate\Exception\RequirementsException; @@ -28,7 +29,8 @@ public function testException() { [], $this->prophesize(MigrationInterface::class)->reveal(), $this->prophesize(ConfigFactoryInterface::class)->reveal(), - $this->prophesize(LanguageManagerInterface::class)->reveal() + $this->prophesize(LanguageManagerInterface::class)->reveal(), + $this->prophesize(TypedConfigManagerInterface::class)->reveal(), ); $this->expectException(RequirementsException::class); $this->expectExceptionMessage("Destination plugin 'test' did not meet the requirements"); diff --git a/core/modules/migrate/tests/src/Unit/destination/ConfigTest.php b/core/modules/migrate/tests/src/Unit/destination/ConfigTest.php index e99dafbcce42cf58d8d84238f31fab834dba284d..b4da80b4fd5f1508b20c3f65cb19a226e7536c5f 100644 --- a/core/modules/migrate/tests/src/Unit/destination/ConfigTest.php +++ b/core/modules/migrate/tests/src/Unit/destination/ConfigTest.php @@ -4,6 +4,7 @@ namespace Drupal\Tests\migrate\Unit\destination; +use Drupal\Core\Config\TypedConfigManagerInterface; use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Plugin\migrate\destination\Config; use Drupal\Tests\UnitTestCase; @@ -35,7 +36,7 @@ public function testImport() { } $config->expects($this->once()) ->method('save'); - $config->expects($this->once()) + $config->expects($this->atLeastOnce()) ->method('getName') ->willReturn('d8_config'); $config_factory = $this->createMock('Drupal\Core\Config\ConfigFactoryInterface'); @@ -56,7 +57,7 @@ public function testImport() { ->method('getLanguageConfigOverride') ->with('fr', 'd8_config') ->willReturn($config); - $destination = new Config(['config_name' => 'd8_config'], 'd8_config', ['pluginId' => 'd8_config'], $migration, $config_factory, $language_manager); + $destination = new Config(['config_name' => 'd8_config'], 'd8_config', ['pluginId' => 'd8_config'], $migration, $config_factory, $language_manager, $this->createMock(TypedConfigManagerInterface::class)); $destination_id = $destination->import($row); $this->assertEquals(['d8_config'], $destination_id); } @@ -106,7 +107,7 @@ public function testLanguageImport() { ->method('getLanguageConfigOverride') ->with('mi', 'd8_config') ->willReturn($config); - $destination = new Config(['config_name' => 'd8_config', 'translations' => 'true'], 'd8_config', ['pluginId' => 'd8_config'], $migration, $config_factory, $language_manager); + $destination = new Config(['config_name' => 'd8_config', 'translations' => 'true'], 'd8_config', ['pluginId' => 'd8_config'], $migration, $config_factory, $language_manager, $this->createMock(TypedConfigManagerInterface::class)); $destination_id = $destination->import($row); $this->assertEquals(['d8_config', 'mi'], $destination_id); } diff --git a/core/modules/system/system.post_update.php b/core/modules/system/system.post_update.php index 952f25d9ede3ee81d15d33378e880f21f2ebb9c6..4a6c601b8c06edc760afc449582b68ab006db60b 100644 --- a/core/modules/system/system.post_update.php +++ b/core/modules/system/system.post_update.php @@ -5,12 +5,17 @@ * Post update functions for System. */ +use Drupal\Core\Config\ConfigManagerInterface; use Drupal\Core\Config\Entity\ConfigEntityUpdater; +use Drupal\Core\Config\Schema\Mapping; +use Drupal\Core\Config\TypedConfigManagerInterface; use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Entity\EntityFormModeInterface; use Drupal\Core\Entity\EntityViewModeInterface; use Drupal\Core\Field\Plugin\Field\FieldFormatter\TimestampFormatter; use Drupal\Core\Site\Settings; +use Drupal\Core\StringTranslation\PluralTranslatableMarkup; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Implements hook_removed_post_updates(). @@ -233,6 +238,70 @@ function system_post_update_sdc_uninstall() { } } +/** + * Adds a langcode to all simple config which needs it. + */ +function system_post_update_add_langcode_to_all_translatable_config(&$sandbox = NULL): TranslatableMarkup { + $config_factory = \Drupal::configFactory(); + + // If this is the first run, populate the sandbox with the names of all + // config objects. + if (!isset($sandbox['names'])) { + $sandbox['names'] = $config_factory->listAll(); + $sandbox['max'] = count($sandbox['names']); + } + + /** @var \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager */ + $typed_config_manager = \Drupal::service(TypedConfigManagerInterface::class); + /** @var \Drupal\Core\Config\ConfigManagerInterface $config_manager */ + $config_manager = \Drupal::service(ConfigManagerInterface::class); + $default_langcode = \Drupal::languageManager()->getDefaultLanguage()->getId(); + + $names = array_splice($sandbox['names'], 0, Settings::get('entity_update_batch_size', 50)); + foreach ($names as $name) { + // We're only dealing with simple config, which won't map to an entity type. + // But if this is a simple config object that has no schema, we can't do + // anything here and we don't need to, because config must have schema in + // order to be translatable. + if ($config_manager->getEntityTypeIdByName($name) || !$typed_config_manager->hasConfigSchema($name)) { + continue; + } + + $config = \Drupal::configFactory()->getEditable($name); + $typed_config = $typed_config_manager->createFromNameAndData($name, $config->getRawData()); + // Simple config is always a mapping. + assert($typed_config instanceof Mapping); + + // If this config contains any elements (at any level of nesting) which + // are translatable, but the config hasn't got a langcode, assign one. But + // if nothing in the config structure is translatable, the config shouldn't + // have a langcode at all. + if ($typed_config->hasTranslatableElements()) { + if ($config->get('langcode')) { + continue; + } + $config->set('langcode', $default_langcode); + } + else { + if (!array_key_exists('langcode', $config->get())) { + continue; + } + $config->clear('langcode'); + } + $config->save(); + } + + $sandbox['#finished'] = empty($sandbox['max']) || empty($sandbox['names']) ? 1 : ($sandbox['max'] - count($sandbox['names'])) / $sandbox['max']; + if ($sandbox['#finished'] === 1) { + return new TranslatableMarkup('Finished updating simple config langcodes.'); + } + return new PluralTranslatableMarkup($sandbox['max'] - count($sandbox['names']), + 'Processed @count items of @total.', + 'Processed @count items of @total.', + ['@total' => $sandbox['max']], + ); +} + /** * Move development settings from state to raw key-value storage. */ diff --git a/core/modules/system/tests/modules/form_test/src/Form/TreeConfigTargetForm.php b/core/modules/system/tests/modules/form_test/src/Form/TreeConfigTargetForm.php index dbf1c427249b299372255fc6321caca025c0ab67..7fc6bed7892ffd6aa2ae74a528db80fd589770de 100644 --- a/core/modules/system/tests/modules/form_test/src/Form/TreeConfigTargetForm.php +++ b/core/modules/system/tests/modules/form_test/src/Form/TreeConfigTargetForm.php @@ -43,6 +43,13 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#title' => t('Nemesis'), '#config_target' => 'form_test.object:nemesis_vegetable', ]; + // Since form_test.object contains translatable values, it must specify a + // language. + $form['langcode'] = [ + '#type' => 'value', + '#value' => 'en', + '#config_target' => 'form_test.object:langcode', + ]; $form['test1'] = [ '#type' => 'select', diff --git a/core/modules/system/tests/modules/menu_test/config/install/menu_test.links.action.yml b/core/modules/system/tests/modules/menu_test/config/install/menu_test.links.action.yml index e0a4853aad803a83b708eef408acdf20ad6298be..f8ba6d651e54b585617db3e8cddd20d2417d1642 100644 --- a/core/modules/system/tests/modules/menu_test/config/install/menu_test.links.action.yml +++ b/core/modules/system/tests/modules/menu_test/config/install/menu_test.links.action.yml @@ -1 +1,2 @@ +langcode: en title: 'Original title' diff --git a/core/modules/system/tests/modules/menu_test/config/install/menu_test.menu_item.yml b/core/modules/system/tests/modules/menu_test/config/install/menu_test.menu_item.yml index ad3ab766d9634636fe5d6178f5501197f5b48b46..3256fb41cbb0b655f319321fa346d7cddf67f1f3 100644 --- a/core/modules/system/tests/modules/menu_test/config/install/menu_test.menu_item.yml +++ b/core/modules/system/tests/modules/menu_test/config/install/menu_test.menu_item.yml @@ -1 +1,2 @@ +langcode: en title: English diff --git a/core/modules/system/tests/src/Functional/Form/ConfigTargetTest.php b/core/modules/system/tests/src/Functional/Form/ConfigTargetTest.php index a4bf46ff398799664dc2d144745c450edeb88986..43771a3f81706ea436c30c844777ab1debd9c5df 100644 --- a/core/modules/system/tests/src/Functional/Form/ConfigTargetTest.php +++ b/core/modules/system/tests/src/Functional/Form/ConfigTargetTest.php @@ -97,6 +97,7 @@ public function testNested(): void { $assert_session->statusMessageContains('The configuration options have been saved.', 'status'); $this->assertSame([ + 'langcode' => 'en', 'favorite_fruits' => [ $most_favorite_fruit, $second_favorite_fruit, @@ -125,6 +126,7 @@ public function testNested(): void { $assert_session->statusMessageContains('The configuration options have been saved.', 'status'); $this->assertSame([ + 'langcode' => 'en', 'favorite_fruits' => [ $most_favorite_fruit, $second_favorite_fruit, @@ -144,6 +146,7 @@ public function testNested(): void { $assert_session->statusMessageContains('The configuration options have been saved.', 'status'); $this->assertSame([ + 'langcode' => 'en', 'favorite_fruits' => [ $most_favorite_fruit, $second_favorite_fruit, diff --git a/core/modules/system/tests/src/Functional/Update/SimpleConfigLangcodeTest.php b/core/modules/system/tests/src/Functional/Update/SimpleConfigLangcodeTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2eab795850f05dda2e8c2d2a5485d5bdbad2c98e --- /dev/null +++ b/core/modules/system/tests/src/Functional/Update/SimpleConfigLangcodeTest.php @@ -0,0 +1,60 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\system\Functional\Update; + +use Drupal\Core\Database\Connection; +use Drupal\FunctionalTests\Update\UpdatePathTestBase; + +/** + * @group system + * @group Update + * @covers system_post_update_add_langcode_to_all_translatable_config + */ +class SimpleConfigLangcodeTest extends UpdatePathTestBase { + + /** + * {@inheritdoc} + */ + protected function setDatabaseDumpFiles() { + $this->databaseDumpFiles = [ + __DIR__ . '/../../../fixtures/update/drupal-9.4.0.bare.standard.php.gz', + ]; + } + + /** + * Tests that langcodes are added to simple config objects that need them. + */ + public function testLangcodesAddedToSimpleConfig(): void { + /** @var \Drupal\Core\Database\Connection $database */ + $database = $this->container->get(Connection::class); + + // Remove the langcode from `user.mail`, which has translatable values; it + // should be restored by the update path. We need to change it in the + // database directly, to avoid running afoul of config validation. + $data = $this->config('user.mail')->clear('langcode')->getRawData(); + $database->update('config') + ->fields([ + 'data' => serialize($data), + ]) + ->condition('name', 'user.mail') + ->execute(); + + // Add a langcode to `node.settings`, which has no translatable values; it + // should be removed by the update path. We need to change it in the + // database directly, to avoid running afoul of config validation. + $data = $this->config('node.settings')->set('langcode', 'en')->getRawData(); + $database->update('config') + ->fields([ + 'data' => serialize($data), + ]) + ->condition('name', 'node.settings') + ->execute(); + + $this->runUpdates(); + $this->assertSame('en', $this->config('user.mail')->get('langcode')); + $this->assertArrayNotHasKey('langcode', $this->config('node.settings')->getRawData()); + } + +} diff --git a/core/modules/system/tests/src/Kernel/Mail/MailTest.php b/core/modules/system/tests/src/Kernel/Mail/MailTest.php index 74edf325259f94eb2c8e14498b18b2ebeec9baf4..e688d3fe4b20ac4b9c1f7cfe2cb265fb78caf283 100644 --- a/core/modules/system/tests/src/Kernel/Mail/MailTest.php +++ b/core/modules/system/tests/src/Kernel/Mail/MailTest.php @@ -44,6 +44,7 @@ protected function setUp(): void { parent::setUp(); $this->installEntitySchema('user'); $this->installEntitySchema('file'); + $this->installConfig(['system']); // Set required site configuration. $this->config('system.site') diff --git a/core/modules/user/tests/src/Kernel/UserAdminSettingsFormTest.php b/core/modules/user/tests/src/Kernel/UserAdminSettingsFormTest.php index 5ea3785e7c0fce4cd87991e33d7140fc93effe1e..4bdad8a594d5f74035cfc77412a6dce6cb20cb0b 100644 --- a/core/modules/user/tests/src/Kernel/UserAdminSettingsFormTest.php +++ b/core/modules/user/tests/src/Kernel/UserAdminSettingsFormTest.php @@ -24,6 +24,7 @@ class UserAdminSettingsFormTest extends ConfigFormTestBase { */ protected function setUp(): void { parent::setUp(); + $this->installConfig(['user']); $this->form = AccountSettingsForm::create($this->container); $this->values = [ diff --git a/core/modules/user/tests/src/Kernel/UserEntityLabelTest.php b/core/modules/user/tests/src/Kernel/UserEntityLabelTest.php index 6594baa7ffb66fcbc191c85e8ec34606bae06fa6..52c34ccfdc6ee36f919d4ec95edbb43590a3769c 100644 --- a/core/modules/user/tests/src/Kernel/UserEntityLabelTest.php +++ b/core/modules/user/tests/src/Kernel/UserEntityLabelTest.php @@ -31,6 +31,7 @@ class UserEntityLabelTest extends KernelTestBase { */ public function testLabelCallback() { $this->installEntitySchema('user'); + $this->installConfig(['user']); $account = $this->createUser(); $anonymous = User::create(['uid' => 0]); diff --git a/core/tests/Drupal/KernelTests/Config/Schema/MappingTest.php b/core/tests/Drupal/KernelTests/Config/Schema/MappingTest.php index 70ae50c01e19ff197ddb4c85bf77d66069306f94..ea1151a10e0c5923f59c78ba21a7d2dfeb71931e 100644 --- a/core/tests/Drupal/KernelTests/Config/Schema/MappingTest.php +++ b/core/tests/Drupal/KernelTests/Config/Schema/MappingTest.php @@ -216,6 +216,7 @@ public static function providerMappingInterpretation(): \Generator { ], [ '_core', + 'langcode', 'profile', ], [], @@ -237,7 +238,7 @@ public static function providerMappingInterpretation(): \Generator { // @see core/modules/config/tests/config_schema_deprecated_test/config/schema/config_schema_deprecated_test.schema.yml 'complex_structure_deprecated', ], - ['_core', 'complex_structure_deprecated'], + ['_core', 'langcode', 'complex_structure_deprecated'], [], ]; yield 'No dynamic type: config_schema_deprecated_test.settings:complex_structure_deprecated' => [ diff --git a/core/tests/Drupal/KernelTests/Config/TypedConfigTest.php b/core/tests/Drupal/KernelTests/Config/TypedConfigTest.php index 1c12076188d21a1d65b82e976366c94c6f61ef29..c26ec8ca8058a8343acf2e7e952c7dc2bac48ffe 100644 --- a/core/tests/Drupal/KernelTests/Config/TypedConfigTest.php +++ b/core/tests/Drupal/KernelTests/Config/TypedConfigTest.php @@ -97,7 +97,7 @@ public function testTypedDataAPI() { $typed_config_manager = \Drupal::service('config.typed'); $typed_config = $typed_config_manager->createFromNameAndData('config_test.validation', \Drupal::configFactory()->get('config_test.validation')->get()); $this->assertInstanceOf(TypedConfigInterface::class, $typed_config); - $this->assertEquals(['_core', 'llama', 'cat', 'giraffe', 'uuid', 'langcode', 'string__not_blank'], array_keys($typed_config->getElements())); + $this->assertEquals(['_core', 'llama', 'cat', 'giraffe', 'uuid', 'string__not_blank'], array_keys($typed_config->getElements())); $this->assertSame('config_test.validation', $typed_config->getName()); $this->assertSame('config_test.validation', $typed_config->getPropertyPath()); $this->assertSame('config_test.validation.llama', $typed_config->get('llama')->getPropertyPath()); diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigLanguageOverrideTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigLanguageOverrideTest.php index a60dc557e4c08baaefd77067528078fea068b384..3b49870ef1bec39811482ae622fadfbb1d1435b9 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigLanguageOverrideTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigLanguageOverrideTest.php @@ -78,6 +78,9 @@ public function testConfigLanguageOverride() { \Drupal::configFactory()->getEditable('config_test.foo') ->set('value', ['key' => 'original']) ->set('label', 'Original') + // `label` is translatable, hence a `langcode` is required. + // @see \Drupal\Core\Config\Plugin\Validation\Constraint\LangcodeRequiredIfTranslatableValuesConstraint + ->set('langcode', 'en') ->save(); \Drupal::languageManager() ->getLanguageConfigOverride('de', 'config_test.foo') diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigModuleOverridesTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigModuleOverridesTest.php index 4fdbbd6a086e8b3cfbc2a12056690c1bc4dc5a8e..12cb8e35a9ef366f3354529e722aea36a2e2e626 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigModuleOverridesTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigModuleOverridesTest.php @@ -32,6 +32,9 @@ public function testSimpleModuleOverrides() { ->getEditable($name) ->set('name', $non_overridden_name) ->set('slogan', $non_overridden_slogan) + // `name` and `slogan` are translatable, hence a `langcode` is required. + // @see \Drupal\Core\Config\Plugin\Validation\Constraint\LangcodeRequiredIfTranslatableValuesConstraint + ->set('langcode', 'en') ->save(); $this->assertEquals($non_overridden_name, $config_factory->get('system.site')->getOriginal('name', FALSE)); diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigOverridesPriorityTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigOverridesPriorityTest.php index 271c313a3135ae5ce3f760c1154ef7a38cc457b5..3e4e5bc0af5481f6856fe16d3b51677ff9de3e71 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigOverridesPriorityTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigOverridesPriorityTest.php @@ -53,6 +53,9 @@ public function testOverridePriorities() { ->set('slogan', $non_overridden_slogan) ->set('mail', $non_overridden_mail) ->set('weight_select_max', 50) + // `name` and `slogan` are translatable, hence a `langcode` is required. + // @see \Drupal\Core\Config\Plugin\Validation\Constraint\LangcodeRequiredIfTranslatableValuesConstraint + ->set('langcode', 'en') ->save(); // Ensure that no overrides are applying. diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php index dfd08b885cb85d5ce99fb031b104106e07803eef..d3bea040889d132ff87a85ba495dd9897a72a042 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php @@ -72,6 +72,7 @@ public function testSchemaMapping() { $expected['label'] = 'Schema test data'; $expected['class'] = Mapping::class; $expected['mapping']['langcode']['type'] = 'langcode'; + $expected['mapping']['langcode']['requiredKey'] = FALSE; $expected['mapping']['_core']['type'] = '_core_config_info'; $expected['mapping']['_core']['requiredKey'] = FALSE; $expected['mapping']['test_item'] = ['label' => 'Test item']; @@ -79,7 +80,10 @@ public function testSchemaMapping() { $expected['type'] = 'config_schema_test.some_schema'; $expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition'; $expected['unwrap_for_canonical_representation'] = TRUE; - $expected['constraints'] = ['ValidKeys' => '<infer>']; + $expected['constraints'] = [ + 'ValidKeys' => '<infer>', + 'LangcodeRequiredIfTranslatableValues' => NULL, + ]; $this->assertEquals($expected, $definition, 'Retrieved the right metadata for configuration with only some schema.'); // Check type detection on elements with undefined types. @@ -123,6 +127,7 @@ public function testSchemaMapping() { $expected['mapping']['langcode'] = [ 'type' => 'langcode', ]; + $expected['mapping']['langcode']['requiredKey'] = FALSE; $expected['mapping']['_core']['type'] = '_core_config_info'; $expected['mapping']['_core']['requiredKey'] = FALSE; $expected['type'] = 'system.maintenance'; @@ -131,6 +136,7 @@ public function testSchemaMapping() { $expected['constraints'] = [ 'ValidKeys' => '<infer>', 'FullyValidatable' => NULL, + 'LangcodeRequiredIfTranslatableValues' => NULL, ]; $this->assertEquals($expected, $definition, 'Retrieved the right metadata for system.maintenance'); @@ -143,6 +149,7 @@ public function testSchemaMapping() { $expected['mapping']['langcode'] = [ 'type' => 'langcode', ]; + $expected['mapping']['langcode']['requiredKey'] = FALSE; $expected['mapping']['_core']['type'] = '_core_config_info'; $expected['mapping']['_core']['requiredKey'] = FALSE; $expected['mapping']['label'] = [ @@ -163,7 +170,10 @@ public function testSchemaMapping() { ]; $expected['type'] = 'config_schema_test.ignore'; $expected['unwrap_for_canonical_representation'] = TRUE; - $expected['constraints'] = ['ValidKeys' => '<infer>']; + $expected['constraints'] = [ + 'ValidKeys' => '<infer>', + 'LangcodeRequiredIfTranslatableValues' => NULL, + ]; $this->assertEquals($expected, $definition); @@ -274,6 +284,7 @@ public function testSchemaMapping() { $expected['label'] = 'Schema multiple filesystem marker test'; $expected['class'] = Mapping::class; $expected['mapping']['langcode']['type'] = 'langcode'; + $expected['mapping']['langcode']['requiredKey'] = FALSE; $expected['mapping']['_core']['type'] = '_core_config_info'; $expected['mapping']['_core']['requiredKey'] = FALSE; $expected['mapping']['test_id']['type'] = 'string'; @@ -283,7 +294,10 @@ public function testSchemaMapping() { $expected['type'] = 'config_schema_test.some_schema.some_module.*.*'; $expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition'; $expected['unwrap_for_canonical_representation'] = TRUE; - $expected['constraints'] = ['ValidKeys' => '<infer>']; + $expected['constraints'] = [ + 'ValidKeys' => '<infer>', + 'LangcodeRequiredIfTranslatableValues' => NULL, + ]; $this->assertEquals($expected, $definition, 'Retrieved the right metadata for config_schema_test.some_schema.some_module.section_one.subsection'); @@ -560,6 +574,7 @@ public function testSchemaFallback() { $expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition'; $expected['unwrap_for_canonical_representation'] = TRUE; $expected['mapping']['langcode']['type'] = 'langcode'; + $expected['mapping']['langcode']['requiredKey'] = FALSE; $expected['mapping']['_core']['type'] = '_core_config_info'; $expected['mapping']['_core']['requiredKey'] = FALSE; $expected['mapping']['test_id']['type'] = 'string'; @@ -567,7 +582,10 @@ public function testSchemaFallback() { $expected['mapping']['test_description']['type'] = 'text'; $expected['mapping']['test_description']['label'] = 'Description'; $expected['type'] = 'config_schema_test.wildcard_fallback.*'; - $expected['constraints'] = ['ValidKeys' => '<infer>']; + $expected['constraints'] = [ + 'ValidKeys' => '<infer>', + 'LangcodeRequiredIfTranslatableValues' => NULL, + ]; $this->assertEquals($expected, $definition, 'Retrieved the right metadata for config_schema_test.wildcard_fallback.something');