diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index 748500fca26af4db635629984e5b6f4aeacd5d75..322a9514f2c4858210409d9bf16a7d5818b12dd5 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -104,6 +104,15 @@ machine_name:
       # @see \Drupal\Core\Config\Entity\ConfigEntityStorage::MAX_ID_LENGTH
       max: 166
 
+# A language identifier.
+langcode:
+  type: string
+  label: 'Language code'
+  constraints:
+    NotNull: []
+    Choice:
+      callback: 'Drupal\Core\TypedData\Plugin\DataType\LanguageReference::getAllValidLangcodes'
+
 # Complex extended data types:
 
 # Root of a configuration object.
@@ -130,8 +139,7 @@ config_object:
     _core:
       type: _core_config_info
     langcode:
-      type: string
-      label: 'Language code'
+      type: langcode
 
 # Mail text with subject and body parts.
 mail:
@@ -305,8 +313,7 @@ config_entity:
       type: uuid
       label: 'UUID'
     langcode:
-      type: string
-      label: 'Language code'
+      type: langcode
     status:
       type: boolean
       label: 'Status'
diff --git a/core/lib/Drupal/Core/TypedData/Plugin/DataType/LanguageReference.php b/core/lib/Drupal/Core/TypedData/Plugin/DataType/LanguageReference.php
index 4f76bda335f11e32475f7b68a47cccd53a008a25..c9caaec1733b308dcfd553f13de5b609a3c48df7 100644
--- a/core/lib/Drupal/Core/TypedData/Plugin/DataType/LanguageReference.php
+++ b/core/lib/Drupal/Core/TypedData/Plugin/DataType/LanguageReference.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Core\TypedData\Plugin\DataType;
 
+use Drupal\Core\Language\LanguageInterface;
 use Drupal\Core\TypedData\DataReferenceBase;
 
 /**
@@ -30,4 +31,34 @@ public function getTargetIdentifier() {
     return isset($language) ? $language->id() : NULL;
   }
 
+  /**
+   * Returns all valid values for a `langcode` config value.
+   *
+   * @return string[]
+   *   All possible valid langcodes. This includes all langcodes in the standard
+   *   list of human languages, along with special langcodes like `und`, `zxx`,
+   *   and `site_default`, which Drupal uses internally. If any custom languages
+   *   are defined, they will be included as well.
+   *
+   * @see \Drupal\Core\Language\LanguageManagerInterface::getLanguages()
+   * @see \Drupal\Core\Language\LanguageManagerInterface::getStandardLanguageList()
+   */
+  public static function getAllValidLangcodes(): array {
+    $language_manager = \Drupal::languageManager();
+
+    return array_unique([
+      ...array_keys($language_manager::getStandardLanguageList()),
+      // We can't use LanguageInterface::STATE_ALL because it will exclude the
+      // site default language in certain situations.
+      // @see \Drupal\Core\Language\LanguageManager::filterLanguages()
+      ...array_keys($language_manager->getLanguages(LanguageInterface::STATE_LOCKED | LanguageInterface::STATE_CONFIGURABLE | LanguageInterface::STATE_SITE_DEFAULT)),
+      // Include special language codes used internally.
+      LanguageInterface::LANGCODE_NOT_APPLICABLE,
+      LanguageInterface::LANGCODE_SITE_DEFAULT,
+      LanguageInterface::LANGCODE_DEFAULT,
+      LanguageInterface::LANGCODE_SYSTEM,
+      LanguageInterface::LANGCODE_NOT_SPECIFIED,
+    ]);
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Validation/ConstraintManager.php b/core/lib/Drupal/Core/Validation/ConstraintManager.php
index c5faab67ed0541fd40fc1806e7970587ba3b16c6..7d024e22bffdebb855a934aead9b10c3bbb67cf5 100644
--- a/core/lib/Drupal/Core/Validation/ConstraintManager.php
+++ b/core/lib/Drupal/Core/Validation/ConstraintManager.php
@@ -7,6 +7,11 @@
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Plugin\DefaultPluginManager;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\Validation\Plugin\Validation\Constraint\EmailConstraint;
+use Symfony\Component\Validator\Constraints\Blank;
+use Symfony\Component\Validator\Constraints\Callback;
+use Symfony\Component\Validator\Constraints\Choice;
+use Symfony\Component\Validator\Constraints\NotBlank;
 
 /**
  * Constraint plugin manager.
@@ -87,24 +92,29 @@ public function create($name, $options) {
   public function registerDefinitions() {
     $this->getDiscovery()->setDefinition('Callback', [
       'label' => new TranslatableMarkup('Callback'),
-      'class' => '\Symfony\Component\Validator\Constraints\Callback',
+      'class' => Callback::class,
       'type' => FALSE,
     ]);
     $this->getDiscovery()->setDefinition('Blank', [
       'label' => new TranslatableMarkup('Blank'),
-      'class' => '\Symfony\Component\Validator\Constraints\Blank',
+      'class' => Blank::class,
       'type' => FALSE,
     ]);
     $this->getDiscovery()->setDefinition('NotBlank', [
       'label' => new TranslatableMarkup('Not blank'),
-      'class' => '\Symfony\Component\Validator\Constraints\NotBlank',
+      'class' => NotBlank::class,
       'type' => FALSE,
     ]);
     $this->getDiscovery()->setDefinition('Email', [
       'label' => new TranslatableMarkup('Email'),
-      'class' => '\Drupal\Core\Validation\Plugin\Validation\Constraint\EmailConstraint',
+      'class' => EmailConstraint::class,
       'type' => ['string'],
     ]);
+    $this->getDiscovery()->setDefinition('Choice', [
+      'label' => new TranslatableMarkup('Choice'),
+      'class' => Choice::class,
+      'type' => FALSE,
+    ]);
   }
 
   /**
diff --git a/core/modules/ckeditor5/ckeditor5.module b/core/modules/ckeditor5/ckeditor5.module
index e2574b833c37033fb243270e053783ffed8cd1fe..a3daee5f264ebde2794f866a2cfe693b54a6b173 100644
--- a/core/modules/ckeditor5/ckeditor5.module
+++ b/core/modules/ckeditor5/ckeditor5.module
@@ -23,7 +23,6 @@
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Url;
 use Drupal\editor\EditorInterface;
-use Symfony\Component\Validator\Constraints\Choice;
 
 /**
  * Implements hook_help().
@@ -568,24 +567,6 @@ function ckeditor5_js_alter(&$javascript, AttachedAssetsInterface $assets, Langu
   }
 }
 
-/**
- * Implements hook_validation_constraint_alter().
- */
-function ckeditor5_validation_constraint_alter(array &$definitions) {
-  // Add the Symfony validation constraints that Drupal core does not add in
-  // \Drupal\Core\Validation\ConstraintManager::registerDefinitions() for
-  // unknown reasons. Do it defensively, to not break when this changes.
-  if (!isset($definitions['Choice'])) {
-    $definitions['Choice'] = [
-      'label' => 'Choice',
-      'class' => Choice::class,
-      'type' => FALSE,
-      'provider' => 'core',
-      'id' => 'Choice',
-    ];
-  }
-}
-
 /**
  * Implements hook_config_schema_info_alter().
  */
diff --git a/core/modules/language/config/schema/language.schema.yml b/core/modules/language/config/schema/language.schema.yml
index 91ce2d19026c7c57eead15f09e9f9f324ceb3dd7..4516a6fe057c202696aa6a2aeba819092880efe8 100644
--- a/core/modules/language/config/schema/language.schema.yml
+++ b/core/modules/language/config/schema/language.schema.yml
@@ -71,7 +71,7 @@ language.negotiation:
             type: string
             label: 'Domain'
     selected_langcode:
-      type: string
+      type: langcode
       label: 'Selected language'
 
 language.mappings:
@@ -118,7 +118,7 @@ language.content_settings.*.*:
       type:  string
       label: 'Bundle'
     default_langcode:
-      type: string
+      type: langcode
       label: 'Default language'
     language_alterable:
       type: boolean
@@ -130,7 +130,7 @@ condition.plugin.language:
     langcodes:
       type: sequence
       sequence:
-        type: string
+        type: langcode
 
 field.widget.settings.language_select:
   type: mapping
diff --git a/core/modules/system/config/schema/system.schema.yml b/core/modules/system/config/schema/system.schema.yml
index 7dcfd4db58e284b7099b4ba5ecf93fe257968a11..2c01a98156c7307e7d57066e4002bdfe201f5c5a 100644
--- a/core/modules/system/config/schema/system.schema.yml
+++ b/core/modules/system/config/schema/system.schema.yml
@@ -39,7 +39,7 @@ system.site:
       type: integer
       label: 'Weight element maximum value'
     default_langcode:
-      type: string
+      type: langcode
       label: 'Site default language code'
     mail_notification:
       type: string
diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php
index 2aa8cedb87442c7868cfd37a7ca2045852b25aad..b0eafccf14db23e7719b379fa45196d35e504579 100644
--- a/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php
+++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php
@@ -4,7 +4,11 @@
 
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Config\Entity\ConfigEntityInterface;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Language\LanguageManager;
+use Drupal\Core\TypedData\Plugin\DataType\LanguageReference;
 use Drupal\KernelTests\KernelTestBase;
+use Drupal\language\Entity\ConfigurableLanguage;
 
 /**
  * Base class for testing validation of config entities.
@@ -311,4 +315,53 @@ protected function assertValidationErrors(array $expected_messages): void {
     $this->assertSame($expected_messages, $actual_messages);
   }
 
+  /**
+   * Tests that the config entity's langcode is validated.
+   */
+  public function testLangcode(): void {
+    $this->entity->set('langcode', NULL);
+    $this->assertValidationErrors([
+      'langcode' => 'This value should not be null.',
+    ]);
+
+    // A langcode from the standard list should always be acceptable.
+    $standard_languages = LanguageManager::getStandardLanguageList();
+    $this->assertNotEmpty($standard_languages);
+    $this->entity->set('langcode', key($standard_languages));
+    $this->assertValidationErrors([]);
+
+    // All special, internal langcodes should be acceptable.
+    $system_langcodes = [
+      LanguageInterface::LANGCODE_NOT_SPECIFIED,
+      LanguageInterface::LANGCODE_NOT_APPLICABLE,
+      LanguageInterface::LANGCODE_DEFAULT,
+      LanguageInterface::LANGCODE_SITE_DEFAULT,
+      LanguageInterface::LANGCODE_SYSTEM,
+    ];
+    foreach ($system_langcodes as $langcode) {
+      $this->entity->set('langcode', $langcode);
+      $this->assertValidationErrors([]);
+    }
+
+    // An invalid langcode should be unacceptable, even if it "looks" right.
+    $fake_langcode = 'definitely-not-a-language';
+    $this->assertArrayNotHasKey($fake_langcode, LanguageReference::getAllValidLangcodes());
+    $this->entity->set('langcode', $fake_langcode);
+    $this->assertValidationErrors([
+      'langcode' => 'The value you selected is not a valid choice.',
+    ]);
+
+    // If a new configurable language is created with a non-standard langcode,
+    // it should be acceptable.
+    $this->enableModules(['language']);
+    // The language doesn't exist yet, so it shouldn't be a valid choice.
+    $this->entity->set('langcode', 'kthxbai');
+    $this->assertValidationErrors([
+      'langcode' => 'The value you selected is not a valid choice.',
+    ]);
+    // Once we create the language, it should be a valid choice.
+    ConfigurableLanguage::createFromLangcode('kthxbai')->save();
+    $this->assertValidationErrors([]);
+  }
+
 }
diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php
index 05e7b54c79a02bda77797d743cd9ac27aad8226d..f04e330a4205751bd4abffe36bc3331e7e9870a7 100644
--- a/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php
@@ -68,8 +68,7 @@ public function testSchemaMapping() {
     $expected = [];
     $expected['label'] = 'Schema test data';
     $expected['class'] = Mapping::class;
-    $expected['mapping']['langcode']['type'] = 'string';
-    $expected['mapping']['langcode']['label'] = 'Language code';
+    $expected['mapping']['langcode']['type'] = 'langcode';
     $expected['mapping']['_core']['type'] = '_core_config_info';
     $expected['mapping']['testitem'] = ['label' => 'Test item'];
     $expected['mapping']['testlist'] = ['label' => 'Test list'];
@@ -115,8 +114,7 @@ public function testSchemaMapping() {
       'type' => 'text',
     ];
     $expected['mapping']['langcode'] = [
-      'label' => 'Language code',
-      'type' => 'string',
+      'type' => 'langcode',
     ];
     $expected['mapping']['_core']['type'] = '_core_config_info';
     $expected['type'] = 'system.maintenance';
@@ -131,8 +129,7 @@ public function testSchemaMapping() {
     $expected['class'] = Mapping::class;
     $expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition';
     $expected['mapping']['langcode'] = [
-      'type' => 'string',
-      'label' => 'Language code',
+      'type' => 'langcode',
     ];
     $expected['mapping']['_core']['type'] = '_core_config_info';
     $expected['mapping']['label'] = [
@@ -179,8 +176,7 @@ public function testSchemaMapping() {
     $expected['mapping']['name']['type'] = 'machine_name';
     $expected['mapping']['uuid']['type'] = 'uuid';
     $expected['mapping']['uuid']['label'] = 'UUID';
-    $expected['mapping']['langcode']['type'] = 'string';
-    $expected['mapping']['langcode']['label'] = 'Language code';
+    $expected['mapping']['langcode']['type'] = 'langcode';
     $expected['mapping']['status']['type'] = 'boolean';
     $expected['mapping']['status']['label'] = 'Status';
     $expected['mapping']['dependencies']['type'] = 'config_dependencies';
@@ -247,8 +243,7 @@ public function testSchemaMapping() {
     $expected = [];
     $expected['label'] = 'Schema multiple filesystem marker test';
     $expected['class'] = Mapping::class;
-    $expected['mapping']['langcode']['type'] = 'string';
-    $expected['mapping']['langcode']['label'] = 'Language code';
+    $expected['mapping']['langcode']['type'] = 'langcode';
     $expected['mapping']['_core']['type'] = '_core_config_info';
     $expected['mapping']['testid']['type'] = 'string';
     $expected['mapping']['testid']['label'] = 'ID';
@@ -518,8 +513,7 @@ public function testSchemaFallback() {
     $expected['class'] = Mapping::class;
     $expected['definition_class'] = '\Drupal\Core\TypedData\MapDataDefinition';
     $expected['unwrap_for_canonical_representation'] = TRUE;
-    $expected['mapping']['langcode']['type'] = 'string';
-    $expected['mapping']['langcode']['label'] = 'Language code';
+    $expected['mapping']['langcode']['type'] = 'langcode';
     $expected['mapping']['_core']['type'] = '_core_config_info';
     $expected['mapping']['testid']['type'] = 'string';
     $expected['mapping']['testid']['label'] = 'ID';