diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index b49c6c7d112637f5028261ded93872bda30128b5..248a07231066c9ab5b28849b7907142994e6cf39 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -382,6 +382,26 @@ core.base_field_override.*.*.*:
   type: field_config_base
   label: 'Base field bundle override'
 
+core.date_format.*:
+  type: config_entity
+  label: 'Date format'
+  mapping:
+    id:
+      type: string
+      label: 'ID'
+    label:
+      type: label
+      label: 'Label'
+    locked:
+      type: boolean
+      label: 'Locked'
+    pattern:
+      type: date_format
+      label: 'PHP date format'
+    langcode:
+      type: string
+      label: 'Default language'
+
 # Schema for the String field type.
 
 field.string.instance_settings:
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 0a6f475dedec2122892f301063d28fb8a8de2018..f26f9afbad0b49c7346a7fd429ca6da28e5be1fd 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -843,6 +843,68 @@ function template_preprocess_time(&$variables) {
   $variables['attributes'] = new Attribute($variables['attributes']);
 }
 
+/**
+ * Prepares variables for datetime form element templates.
+ *
+ * The datetime form element serves as a wrapper around the date element type,
+ * which creates a date and a time component for a date.
+ *
+ * Default template: datetime-form.html.twig.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #title, #value, #options, #description, #required,
+ *     #attributes.
+ *
+ * @see form_process_datetime()
+ */
+function template_preprocess_datetime_form(&$variables) {
+  $element = $variables['element'];
+
+  $variables['attributes'] = array();
+  if (isset($element['#id'])) {
+    $variables['attributes']['id'] = $element['#id'];
+  }
+  if (!empty($element['#attributes']['class'])) {
+    $variables['attributes']['class'] = (array) $element['#attributes']['class'];
+  }
+  $variables['attributes']['class'][] = 'container-inline';
+
+  $variables['content'] = $element;
+}
+
+/**
+ * Prepares variables for datetime form wrapper templates.
+ *
+ * Default template: datetime-wrapper.html.twig.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - element: An associative array containing the properties of the element.
+ *     Properties used: #title, #children, #required, #attributes.
+ */
+function template_preprocess_datetime_wrapper(&$variables) {
+  $element = $variables['element'];
+
+  if (!empty($element['#title'])) {
+    $variables['title'] = $element['#title'];
+  }
+
+  if (!empty($element['#description'])) {
+    $variables['description'] = $element['#description'];
+  }
+
+  $title_attributes = array('class' => array('label'));
+  // For required datetime fields a 'form-required' class is appended to the
+  // label attributes.
+  if (!empty($element['#required'])) {
+    $title_attributes['class'][] = 'form-required';
+  }
+  $variables['title_attributes'] = new Attribute($title_attributes);
+  $variables['content'] = $element['#children'];
+}
+
 /**
  * Prepares variables for status message templates.
  *
@@ -2183,6 +2245,14 @@ function drupal_common_theme() {
       'variables' => array('timestamp' => NULL, 'text' => NULL, 'attributes' => array(), 'html' => FALSE),
       'template' => 'time',
     ),
+    'datetime_form' => array(
+      'template' => 'datetime-form',
+      'render element' => 'element',
+    ),
+    'datetime_wrapper' => array(
+      'template' => 'datetime-wrapper',
+      'render element' => 'element',
+    ),
     'status_messages' => array(
       'variables' => array('display' => NULL),
       'template' => 'status-messages',
diff --git a/core/modules/system/src/DateFormatInterface.php b/core/lib/Drupal/Core/Datetime/DateFormatInterface.php
similarity index 90%
rename from core/modules/system/src/DateFormatInterface.php
rename to core/lib/Drupal/Core/Datetime/DateFormatInterface.php
index fe1353d1a71c51b1007c661e426903c7bf1386cd..162ec3bf0469355d6aa409047327799ac30ccd97 100644
--- a/core/modules/system/src/DateFormatInterface.php
+++ b/core/lib/Drupal/Core/Datetime/DateFormatInterface.php
@@ -2,10 +2,10 @@
 
 /**
  * @file
- * Contains \Drupal\system\DateFormatInterface.
+ * Contains \Drupal\Core\Datetime\DateFormatInterface.
  */
 
-namespace Drupal\system;
+namespace Drupal\Core\Datetime;
 
 use Drupal\Core\Config\Entity\ConfigEntityInterface;
 use Drupal\Core\Datetime\DrupalDateTime;
diff --git a/core/modules/datetime/src/DateHelper.php b/core/lib/Drupal/Core/Datetime/DateHelper.php
similarity index 99%
rename from core/modules/datetime/src/DateHelper.php
rename to core/lib/Drupal/Core/Datetime/DateHelper.php
index f90d2c09445c8bc9921625431172c17f251b1657..1357267f5805e5cc41a0d207f28282880f68e9a4 100644
--- a/core/modules/datetime/src/DateHelper.php
+++ b/core/lib/Drupal/Core/Datetime/DateHelper.php
@@ -1,7 +1,7 @@
 <?php
 /**
  * @file
- * Contains \Drupal\datetime\DateHelper.
+ * Contains \Drupal\Core\Datetime\DateHelper.
  *
  * Lots of helpful functions for use in massaging dates, specific to the the
  * Gregorian calendar system. The values include both translated and
@@ -14,7 +14,7 @@
  * translation should be hard-coded and wrapped in t() so the translation system
  * will be able to process them.
  */
-namespace Drupal\datetime;
+namespace Drupal\Core\Datetime;
 
 use Drupal\Core\Datetime\DrupalDateTime;
 
diff --git a/core/lib/Drupal/Core/Datetime/Element/DateElementBase.php b/core/lib/Drupal/Core/Datetime/Element/DateElementBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..56eb9cf7b48ef9d8e3660bbdbb3d95186e672f9c
--- /dev/null
+++ b/core/lib/Drupal/Core/Datetime/Element/DateElementBase.php
@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Datetime\Element\DateElementBase.
+ */
+
+namespace Drupal\Core\Datetime\Element;
+
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\Core\Render\Element\FormElement;
+
+/**
+ * Provides a base class for date elements.
+ */
+abstract class DateElementBase extends FormElement {
+
+  /**
+   * Specifies the start and end year to use as a date range.
+   *
+   * Handles a string like -3:+3 or 2001:2010 to describe a dynamic range of
+   * minimum and maximum years to use in a date selector.
+   *
+   * Centers the range around the current year, if any, but expands it far enough
+   * so it will pick up the year value in the field in case the value in the field
+   * is outside the initial range.
+   *
+   * @param string $string
+   *   A min and max year string like '-3:+1' or '2000:2010' or '2000:+3'.
+   * @param object $date
+   *   (optional) A date object to test as a default value. Defaults to NULL.
+   *
+   * @return array
+   *   A numerically indexed array, containing the minimum and maximum year
+   *   described by this pattern.
+   */
+  protected static function datetimeRangeYears($string, $date = NULL) {
+    $this_year = date_format(new DrupalDateTime(), 'Y');
+    list($min_year, $max_year) = explode(':', $string);
+
+    // Valid patterns would be -5:+5, 0:+1, 2008:2010.
+    $plus_pattern = '@[\+|\-][0-9]{1,4}@';
+    $year_pattern = '@^[0-9]{4}@';
+    if (!preg_match($year_pattern, $min_year, $matches)) {
+      if (preg_match($plus_pattern, $min_year, $matches)) {
+        $min_year = $this_year + $matches[0];
+      }
+      else {
+        $min_year = $this_year;
+      }
+    }
+    if (!preg_match($year_pattern, $max_year, $matches)) {
+      if (preg_match($plus_pattern, $max_year, $matches)) {
+        $max_year = $this_year + $matches[0];
+      }
+      else {
+        $max_year = $this_year;
+      }
+    }
+    // We expect the $min year to be less than the $max year. Some custom values
+    // for -99:+99 might not obey that.
+    if ($min_year > $max_year) {
+      $temp = $max_year;
+      $max_year = $min_year;
+      $min_year = $temp;
+    }
+    // If there is a current value, stretch the range to include it.
+    $value_year = $date instanceOf DrupalDateTime ? $date->format('Y') : '';
+    if (!empty($value_year)) {
+      $min_year = min($value_year, $min_year);
+      $max_year = max($value_year, $max_year);
+    }
+    return array($min_year, $max_year);
+  }
+
+}
+
diff --git a/core/lib/Drupal/Core/Datetime/Element/Datelist.php b/core/lib/Drupal/Core/Datetime/Element/Datelist.php
new file mode 100644
index 0000000000000000000000000000000000000000..c330a4f8cbc8060e3253f28ee5673da99a21c14d
--- /dev/null
+++ b/core/lib/Drupal/Core/Datetime/Element/Datelist.php
@@ -0,0 +1,361 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Datetime\Element\Datelist.
+ */
+
+namespace Drupal\Core\Datetime\Element;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Datetime\DateHelper;
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides a datelist element.
+ *
+ * @FormElement("datelist")
+ */
+class Datelist extends DateElementBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    $class = get_class($this);
+    return array(
+      '#input' => TRUE,
+      '#element_validate' => array(
+        array($class, 'validateDatelist'),
+      ),
+      '#process' => array(
+        array($class, 'processDatelist'),
+      ),
+      '#theme' => 'datetime_form',
+      '#theme_wrappers' => array('datetime_wrapper'),
+      '#date_part_order' => array('year', 'month', 'day', 'hour', 'minute'),
+      '#date_year_range' => '1900:2050',
+      '#date_increment' => 1,
+      '#date_date_callbacks' => array(),
+      '#date_timezone' => '',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * Validates the date type to adjust 12 hour time and prevent invalid dates.
+   * If the date is valid, the date is set in the form.
+   */
+  public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
+    $parts = $element['#date_part_order'];
+    $increment = $element['#date_increment'];
+
+    $date = NULL;
+    if ($input !== FALSE) {
+      $return = $input;
+      if (isset($input['ampm'])) {
+        if ($input['ampm'] == 'pm' && $input['hour'] < 12) {
+          $input['hour'] += 12;
+        }
+        elseif ($input['ampm'] == 'am' && $input['hour'] == 12) {
+          $input['hour'] -= 12;
+        }
+        unset($input['ampm']);
+      }
+      $timezone = !empty($element['#date_timezone']) ? $element['#date_timezone'] : NULL;
+      $date = DrupalDateTime::createFromArray($input, $timezone);
+      if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
+        static::incrementRound($date, $increment);
+      }
+    }
+    else {
+      $return = array_fill_keys($parts, '');
+      if (!empty($element['#default_value'])) {
+        $date = $element['#default_value'];
+        if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
+          static::incrementRound($date, $increment);
+          foreach ($parts as $part) {
+            switch ($part) {
+              case 'day':
+                $format = 'j';
+                break;
+
+              case 'month':
+                $format = 'n';
+                break;
+
+              case 'year':
+                $format = 'Y';
+                break;
+
+              case 'hour':
+                $format = in_array('ampm', $element['#date_part_order']) ? 'g': 'G';
+                break;
+
+              case 'minute':
+                $format = 'i';
+                break;
+
+              case 'second':
+                $format = 's';
+                break;
+
+              case 'ampm':
+                $format = 'a';
+                break;
+
+              default:
+                $format = '';
+
+            }
+            $return[$part] = $date->format($format);
+          }
+        }
+      }
+    }
+    $return['object'] = $date;
+    return $return;
+  }
+
+  /**
+   * Expands a date element into an array of individual elements.
+   *
+   * Required settings:
+   *   - #default_value: A DrupalDateTime object, adjusted to the proper local
+   *     timezone. Converting a date stored in the database from UTC to the local
+   *     zone and converting it back to UTC before storing it is not handled here.
+   *     This element accepts a date as the default value, and then converts the
+   *     user input strings back into a new date object on submission. No timezone
+   *     adjustment is performed.
+   * Optional properties include:
+   *   - #date_part_order: Array of date parts indicating the parts and order
+   *     that should be used in the selector, optionally including 'ampm' for
+   *     12 hour time. Default is array('year', 'month', 'day', 'hour', 'minute').
+   *   - #date_text_parts: Array of date parts that should be presented as
+   *     text fields instead of drop-down selectors. Default is an empty array.
+   *   - #date_date_callbacks: Array of optional callbacks for the date element.
+   *   - #date_year_range: A description of the range of years to allow, like
+   *     '1900:2050', '-3:+3' or '2000:+3', where the first value describes the
+   *     earliest year and the second the latest year in the range. A year
+   *     in either position means that specific year. A +/- value describes a
+   *     dynamic value that is that many years earlier or later than the current
+   *     year at the time the form is displayed. Defaults to '1900:2050'.
+   *   - #date_increment: The increment to use for minutes and seconds, i.e.
+   *     '15' would show only :00, :15, :30 and :45. Defaults to 1 to show every
+   *     minute.
+   *   - #date_timezone: The local timezone to use when creating dates. Generally
+   *     this should be left empty and it will be set correctly for the user using
+   *     the form. Useful if the default value is empty to designate a desired
+   *     timezone for dates created in form processing. If a default date is
+   *     provided, this value will be ignored, the timezone in the default date
+   *     takes precedence. Defaults to the value returned by
+   *     drupal_get_user_timezone().
+   *
+   * Example usage:
+   * @code
+   *   $form = array(
+   *     '#type' => 'datelist',
+   *     '#default_value' => new DrupalDateTime('2000-01-01 00:00:00'),
+   *     '#date_part_order' => array('month', 'day', 'year', 'hour', 'minute', 'ampm'),
+   *     '#date_text_parts' => array('year'),
+   *     '#date_year_range' => '2010:2020',
+   *     '#date_increment' => 15,
+   *   );
+   * @endcode
+   *
+   * @param array $element
+   *   The form element whose value is being processed.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   * @param array $complete_form
+   *   The complete form structure.
+   *
+   * @return array
+   */
+  public static function processDatelist(&$element, FormStateInterface $form_state, &$complete_form) {
+    // Load translated date part labels from the appropriate calendar plugin.
+    $date_helper = new DateHelper();
+
+    // The value callback has populated the #value array.
+    $date = !empty($element['#value']['object']) ? $element['#value']['object'] : NULL;
+
+    // Set a fallback timezone.
+    if ($date instanceOf DrupalDateTime) {
+      $element['#date_timezone'] = $date->getTimezone()->getName();
+    }
+    elseif (!empty($element['#timezone'])) {
+      $element['#date_timezone'] = $element['#date_timezone'];
+    }
+    else {
+      $element['#date_timezone'] = drupal_get_user_timezone();
+    }
+
+    $element['#tree'] = TRUE;
+
+    // Determine the order of the date elements.
+    $order = !empty($element['#date_part_order']) ? $element['#date_part_order'] : array('year', 'month', 'day');
+    $text_parts = !empty($element['#date_text_parts']) ? $element['#date_text_parts'] : array();
+
+    // Output multi-selector for date.
+    foreach ($order as $part) {
+      switch ($part) {
+        case 'day':
+          $options = $date_helper->days($element['#required']);
+          $format = 'j';
+          $title = t('Day');
+          break;
+
+        case 'month':
+          $options = $date_helper->monthNamesAbbr($element['#required']);
+          $format = 'n';
+          $title = t('Month');
+          break;
+
+        case 'year':
+          $range = static::datetimeRangeYears($element['#date_year_range'], $date);
+          $options = $date_helper->years($range[0], $range[1], $element['#required']);
+          $format = 'Y';
+          $title = t('Year');
+          break;
+
+        case 'hour':
+          $format = in_array('ampm', $element['#date_part_order']) ? 'g': 'G';
+          $options = $date_helper->hours($format, $element['#required']);
+          $title = t('Hour');
+          break;
+
+        case 'minute':
+          $format = 'i';
+          $options = $date_helper->minutes($format, $element['#required'], $element['#date_increment']);
+          $title = t('Minute');
+          break;
+
+        case 'second':
+          $format = 's';
+          $options = $date_helper->seconds($format, $element['#required'], $element['#date_increment']);
+          $title = t('Second');
+          break;
+
+        case 'ampm':
+          $format = 'a';
+          $options = $date_helper->ampm($element['#required']);
+          $title = t('AM/PM');
+          break;
+
+        default:
+          $format = '';
+          $options = array();
+          $title = '';
+      }
+
+      $default = !empty($element['#value'][$part]) ? $element['#value'][$part] : '';
+      $value = $date instanceOf DrupalDateTime && !$date->hasErrors() ? $date->format($format) : $default;
+      if (!empty($value) && $part != 'ampm') {
+        $value = intval($value);
+      }
+
+      $element['#attributes']['title'] = $title;
+      $element[$part] = array(
+        '#type' => in_array($part, $text_parts) ? 'textfield' : 'select',
+        '#title' => $title,
+        '#title_display' => 'invisible',
+        '#value' => $value,
+        '#attributes' => $element['#attributes'],
+        '#options' => $options,
+        '#required' => $element['#required'],
+      );
+    }
+
+    // Allows custom callbacks to alter the element.
+    if (!empty($element['#date_date_callbacks'])) {
+      foreach ($element['#date_date_callbacks'] as $callback) {
+        if (function_exists($callback)) {
+          $callback($element, $form_state, $date);
+        }
+      }
+    }
+
+    return $element;
+  }
+
+  /**
+   * Validation callback for a datelist element.
+   *
+   * If the date is valid, the date object created from the user input is set in
+   * the form for use by the caller. The work of compiling the user input back
+   * into a date object is handled by the value callback, so we can use it here.
+   * We also have the raw input available for validation testing.
+   *
+   * @param array $element
+   *   The element being processed.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   * @param array $complete_form
+   *   The complete form structure.
+   */
+  public static function validateDatelist(&$element, FormStateInterface $form_state, &$complete_form) {
+    $input_exists = FALSE;
+    $input = NestedArray::getValue($form_state->getValues(), $element['#parents'], $input_exists);
+    if ($input_exists) {
+
+      // If there's empty input and the field is not required, set it to empty.
+      if (empty($input['year']) && empty($input['month']) && empty($input['day']) && !$element['#required']) {
+        $form_state->setValueForElement($element, NULL);
+      }
+      // If there's empty input and the field is required, set an error.
+      elseif (empty($input['year']) && empty($input['month']) && empty($input['day']) && $element['#required']) {
+        $form_state->setError($element, t('The %field date is required.'));
+      }
+      else {
+        // If the input is valid, set it.
+        $date = $input['object'];
+        if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
+          $form_state->setValueForElement($element, $date);
+        }
+        // If the input is invalid, set an error.
+        else {
+          $form_state->setError($element, t('The %field date is invalid.'));
+        }
+      }
+    }
+  }
+
+  /**
+   * Rounds minutes and seconds to nearest requested value.
+   *
+   * @param $date
+   *
+   * @param $increment
+   *
+   * @return
+   */
+  protected static function incrementRound(&$date, $increment) {
+    // Round minutes and seconds, if necessary.
+    if ($date instanceOf DrupalDateTime && $increment > 1) {
+      $day = intval(date_format($date, 'j'));
+      $hour = intval(date_format($date, 'H'));
+      $second = intval(round(intval(date_format($date, 's')) / $increment) * $increment);
+      $minute = intval(date_format($date, 'i'));
+      if ($second == 60) {
+        $minute += 1;
+        $second = 0;
+      }
+      $minute = intval(round($minute / $increment) * $increment);
+      if ($minute == 60) {
+        $hour += 1;
+        $minute = 0;
+      }
+      date_time_set($date, $hour, $minute, $second);
+      if ($hour == 24) {
+        $day += 1;
+        $year = date_format($date, 'Y');
+        $month = date_format($date, 'n');
+        date_date_set($date, $year, $month, $day);
+      }
+    }
+    return $date;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Datetime/Element/Datetime.php b/core/lib/Drupal/Core/Datetime/Element/Datetime.php
new file mode 100644
index 0000000000000000000000000000000000000000..583372b3dd868866c6a9531087319e802f8bef62
--- /dev/null
+++ b/core/lib/Drupal/Core/Datetime/Element/Datetime.php
@@ -0,0 +1,431 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Datetime\Element\Datetime.
+ */
+
+namespace Drupal\Core\Datetime\Element;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Datetime\Entity\DateFormat;
+
+/**
+ * Provides a datetime element.
+ *
+ * @FormElement("datetime")
+ */
+class Datetime extends DateElementBase {
+
+  /**
+   * @var \DateTimeInterface
+   */
+  protected static $dateExample;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    $date_format = '';
+    $time_format = '';
+    // Date formats cannot be loaded during install or update.
+    if (!defined('MAINTENANCE_MODE')) {
+      if ($date_format_entity = DateFormat::load('html_date')) {
+        /** @var $date_format_entity \Drupal\Core\Datetime\DateFormatInterface */
+        $date_format = $date_format_entity->getPattern();
+      }
+      if ($time_format_entity = DateFormat::load('html_time')) {
+        /** @var $time_format_entity \Drupal\Core\Datetime\DateFormatInterface */
+        $time_format = $time_format_entity->getPattern();
+      }
+    }
+
+    $class = get_class($this);
+    return array(
+      '#input' => TRUE,
+      '#element_validate' => array(
+        array($class, 'validateDatetime'),
+      ),
+      '#process' => array(
+        array($class, 'processDatetime'),
+        array($class, 'processGroup'),
+      ),
+      '#pre_render' => array(
+        array($class, 'preRenderGroup'),
+      ),
+      '#theme' => 'datetime_form',
+      '#theme_wrappers' => array('datetime_wrapper'),
+      '#date_date_format' => $date_format,
+      '#date_date_element' => 'date',
+      '#date_date_callbacks' => array(),
+      '#date_time_format' => $time_format,
+      '#date_time_element' => 'time',
+      '#date_time_callbacks' => array(),
+      '#date_year_range' => '1900:2050',
+      '#date_increment' => 1,
+      '#date_timezone' => '',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
+    if ($input !== FALSE) {
+      $date_input  = $element['#date_date_element'] != 'none' && !empty($input['date']) ? $input['date'] : '';
+      $time_input  = $element['#date_time_element'] != 'none' && !empty($input['time']) ? $input['time'] : '';
+      $date_format = $element['#date_date_element'] != 'none' ? static::getHtml5DateFormat($element) : '';
+      $time_format = $element['#date_time_element'] != 'none' ? static::getHtml5TimeFormat($element) : '';
+      $timezone = !empty($element['#date_timezone']) ? $element['#date_timezone'] : NULL;
+
+      // Seconds will be omitted in a post in case there's no entry.
+      if (!empty($time_input) && strlen($time_input) == 5) {
+        $time_input .= ':00';
+      }
+
+      try {
+        $date_time_format = trim($date_format . ' ' . $time_format);
+        $date_time_input = trim($date_input . ' ' . $time_input);
+        $date = DrupalDateTime::createFromFormat($date_time_format, $date_time_input, $timezone);
+      }
+      catch (\Exception $e) {
+        $date = NULL;
+      }
+      $input = array(
+        'date'   => $date_input,
+        'time'   => $time_input,
+        'object' => $date,
+      );
+    }
+    else {
+      $date = $element['#default_value'];
+      if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
+        $input = array(
+          'date'   => $date->format($element['#date_date_format']),
+          'time'   => $date->format($element['#date_time_format']),
+          'object' => $date,
+        );
+      }
+      else {
+        $input = array(
+          'date'   => '',
+          'time'   => '',
+          'object' => NULL,
+        );
+      }
+    }
+    return $input;
+  }
+
+  /**
+   * Expands a datetime element type into date and/or time elements.
+   *
+   * All form elements are designed to have sane defaults so any or all can be
+   * omitted. Both the date and time components are configurable so they can be
+   * output as HTML5 datetime elements or not, as desired.
+   *
+   * Examples of possible configurations include:
+   *   HTML5 date and time:
+   *     #date_date_element = 'date';
+   *     #date_time_element = 'time';
+   *   HTML5 datetime:
+   *     #date_date_element = 'datetime';
+   *     #date_time_element = 'none';
+   *   HTML5 time only:
+   *     #date_date_element = 'none';
+   *     #date_time_element = 'time'
+   *   Non-HTML5:
+   *     #date_date_element = 'text';
+   *     #date_time_element = 'text';
+   *
+   * Required settings:
+   *   - #default_value: A DrupalDateTime object, adjusted to the proper local
+   *     timezone. Converting a date stored in the database from UTC to the local
+   *     zone and converting it back to UTC before storing it is not handled here.
+   *     This element accepts a date as the default value, and then converts the
+   *     user input strings back into a new date object on submission. No timezone
+   *     adjustment is performed.
+   * Optional properties include:
+   *   - #date_date_format: A date format string that describes the format that
+   *     should be displayed to the end user for the date. When using HTML5
+   *     elements the format MUST use the appropriate HTML5 format for that
+   *     element, no other format will work. See the format_date() function for a
+   *     list of the possible formats and HTML5 standards for the HTML5
+   *     requirements. Defaults to the right HTML5 format for the chosen element
+   *     if a HTML5 element is used, otherwise defaults to
+   *     entity_load('date_format', 'html_date')->getPattern().
+   *   - #date_date_element: The date element. Options are:
+   *     - datetime: Use the HTML5 datetime element type.
+   *     - datetime-local: Use the HTML5 datetime-local element type.
+   *     - date: Use the HTML5 date element type.
+   *     - text: No HTML5 element, use a normal text field.
+   *     - none: Do not display a date element.
+   *   - #date_date_callbacks: Array of optional callbacks for the date element.
+   *     Can be used to add a jQuery datepicker.
+   *   - #date_time_element: The time element. Options are:
+   *     - time: Use a HTML5 time element type.
+   *     - text: No HTML5 element, use a normal text field.
+   *     - none: Do not display a time element.
+   *   - #date_time_format: A date format string that describes the format that
+   *     should be displayed to the end user for the time. When using HTML5
+   *     elements the format MUST use the appropriate HTML5 format for that
+   *     element, no other format will work. See the format_date() function for
+   *     a list of the possible formats and HTML5 standards for the HTML5
+   *     requirements. Defaults to the right HTML5 format for the chosen element
+   *     if a HTML5 element is used, otherwise defaults to
+   *     entity_load('date_format', 'html_time')->getPattern().
+   *   - #date_time_callbacks: An array of optional callbacks for the time
+   *     element. Can be used to add a jQuery timepicker or an 'All day' checkbox.
+   *   - #date_year_range: A description of the range of years to allow, like
+   *     '1900:2050', '-3:+3' or '2000:+3', where the first value describes the
+   *     earliest year and the second the latest year in the range. A year
+   *     in either position means that specific year. A +/- value describes a
+   *     dynamic value that is that many years earlier or later than the current
+   *     year at the time the form is displayed. Used in jQueryUI datepicker year
+   *     range and HTML5 min/max date settings. Defaults to '1900:2050'.
+   *   - #date_increment: The increment to use for minutes and seconds, i.e.
+   *    '15' would show only :00, :15, :30 and :45. Used for HTML5 step values and
+   *     jQueryUI datepicker settings. Defaults to 1 to show every minute.
+   *   - #date_timezone: The local timezone to use when creating dates. Generally
+   *     this should be left empty and it will be set correctly for the user using
+   *     the form. Useful if the default value is empty to designate a desired
+   *     timezone for dates created in form processing. If a default date is
+   *     provided, this value will be ignored, the timezone in the default date
+   *     takes precedence. Defaults to the value returned by
+   *     drupal_get_user_timezone().
+   *
+   * Example usage:
+   * @code
+   *   $form = array(
+   *     '#type' => 'datetime',
+   *     '#default_value' => new DrupalDateTime('2000-01-01 00:00:00'),
+   *     '#date_date_element' => 'date',
+   *     '#date_time_element' => 'none',
+   *     '#date_year_range' => '2010:+3',
+   *   );
+   * @endcode
+   *
+   * @param array $element
+   *   The form element whose value is being processed.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   * @param array $complete_form
+   *   The complete form structure.
+   *
+   * @return array
+   *   The form element whose value has been processed.
+   */
+  public static function processDatetime(&$element, FormStateInterface $form_state, &$complete_form) {
+    $format_settings = array();
+    // The value callback has populated the #value array.
+    $date = !empty($element['#value']['object']) ? $element['#value']['object'] : NULL;
+
+    // Set a fallback timezone.
+    if ($date instanceOf DrupalDateTime) {
+      $element['#date_timezone'] = $date->getTimezone()->getName();
+    }
+    elseif (empty($element['#timezone'])) {
+      $element['#date_timezone'] = drupal_get_user_timezone();
+    }
+
+    $element['#tree'] = TRUE;
+
+    if ($element['#date_date_element'] != 'none') {
+
+      $date_format = $element['#date_date_element'] != 'none' ? static::getHtml5DateFormat($element) : '';
+      $date_value = !empty($date) ? $date->format($date_format, $format_settings) : $element['#value']['date'];
+
+      // Creating format examples on every individual date item is messy, and
+      // placeholders are invalid for HTML5 date and datetime, so an example
+      // format is appended to the title to appear in tooltips.
+      $extra_attributes = array(
+        'title' => t('Date (i.e. !format)', array('!format' => static::formatExample($date_format))),
+        'type' => $element['#date_date_element'],
+      );
+
+      // Adds the HTML5 date attributes.
+      if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
+        $html5_min = clone($date);
+        $range = static::datetimeRangeYears($element['#date_year_range'], $date);
+        $html5_min->setDate($range[0], 1, 1)->setTime(0, 0, 0);
+        $html5_max = clone($date);
+        $html5_max->setDate($range[1], 12, 31)->setTime(23, 59, 59);
+
+        $extra_attributes += array(
+          'min' => $html5_min->format($date_format, $format_settings),
+          'max' => $html5_max->format($date_format, $format_settings),
+        );
+      }
+
+      $element['date'] = array(
+        '#type' => 'date',
+        '#title' => t('Date'),
+        '#title_display' => 'invisible',
+        '#value' => $date_value,
+        '#attributes' => $element['#attributes'] + $extra_attributes,
+        '#required' => $element['#required'],
+        '#size' => max(12, strlen($element['#value']['date'])),
+      );
+
+      // Allows custom callbacks to alter the element.
+      if (!empty($element['#date_date_callbacks'])) {
+        foreach ($element['#date_date_callbacks'] as $callback) {
+          if (function_exists($callback)) {
+            $callback($element, $form_state, $date);
+          }
+        }
+      }
+    }
+
+    if ($element['#date_time_element'] != 'none') {
+
+      $time_format = $element['#date_time_element'] != 'none' ? static::getHtml5TimeFormat($element) : '';
+      $time_value = !empty($date) ? $date->format($time_format, $format_settings) : $element['#value']['time'];
+
+      // Adds the HTML5 attributes.
+      $extra_attributes = array(
+        'title' => t('Time (i.e. !format)', array('!format' => static::formatExample($time_format))),
+        'type' => $element['#date_time_element'],
+        'step' => $element['#date_increment'],
+      );
+      $element['time'] = array(
+        '#type' => 'date',
+        '#title' => t('Time'),
+        '#title_display' => 'invisible',
+        '#value' => $time_value,
+        '#attributes' => $element['#attributes'] + $extra_attributes,
+        '#required' => $element['#required'],
+        '#size' => 12,
+      );
+
+      // Allows custom callbacks to alter the element.
+      if (!empty($element['#date_time_callbacks'])) {
+        foreach ($element['#date_time_callbacks'] as $callback) {
+          if (function_exists($callback)) {
+            $callback($element, $form_state, $date);
+          }
+        }
+      }
+    }
+
+    return $element;
+  }
+
+  /**
+   * Validation callback for a datetime element.
+   *
+   * If the date is valid, the date object created from the user input is set in
+   * the form for use by the caller. The work of compiling the user input back
+   * into a date object is handled by the value callback, so we can use it here.
+   * We also have the raw input available for validation testing.
+   *
+   * @param array $element
+   *   The form element whose value is being validated.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   * @param array $complete_form
+   *   The complete form structure.
+   */
+  public static function validateDatetime(&$element, FormStateInterface $form_state, &$complete_form) {
+    $input_exists = FALSE;
+    $input = NestedArray::getValue($form_state['values'], $element['#parents'], $input_exists);
+    if ($input_exists) {
+
+      $title = !empty($element['#title']) ? $element['#title'] : '';
+      $date_format = $element['#date_date_element'] != 'none' ? static::getHtml5DateFormat($element) : '';
+      $time_format = $element['#date_time_element'] != 'none' ? static::getHtml5TimeFormat($element) : '';
+      $format = trim($date_format . ' ' . $time_format);
+
+      // If there's empty input and the field is not required, set it to empty.
+      if (empty($input['date']) && empty($input['time']) && !$element['#required']) {
+        $form_state->setValueForElement($element, NULL);
+      }
+      // If there's empty input and the field is required, set an error. A
+      // reminder of the required format in the message provides a good UX.
+      elseif (empty($input['date']) && empty($input['time']) && $element['#required']) {
+        $form_state->setError($element, t('The %field date is required. Please enter a date in the format %format.', array('%field' => $title, '%format' => static::formatExample($format))));
+      }
+      else {
+        // If the date is valid, set it.
+        $date = $input['object'];
+        if ($date instanceof DrupalDateTime && !$date->hasErrors()) {
+          $form_state->setValueForElement($element, $date);
+        }
+        // If the date is invalid, set an error. A reminder of the required
+        // format in the message provides a good UX.
+        else {
+          $form_state->setError($element, t('The %field date is invalid. Please enter a date in the format %format.', array('%field' => $title, '%format' => static::formatExample($format))));
+        }
+      }
+    }
+  }
+
+  /**
+   * Creates an example for a date format.
+   *
+   * This is centralized for a consistent method of creating these examples.
+   *
+   * @param string $format
+   *
+   * @return string
+   */
+  public static function formatExample($format) {
+    if (!static::$dateExample) {
+      static::$dateExample = new DrupalDateTime();
+    }
+    return static::$dateExample->format($format);
+  }
+
+  /**
+   * Retrieves the right format for a HTML5 date element.
+   *
+   * The format is important because these elements will not work with any other
+   * format.
+   *
+   * @param string $element
+   *   The $element to assess.
+   *
+   * @return string
+   *   Returns the right format for the date element, or the original format
+   *   if this is not a HTML5 element.
+   */
+  protected static function getHtml5DateFormat($element) {
+    switch ($element['#date_date_element']) {
+      case 'date':
+        return DateFormat::load('html_date')->getPattern();
+
+      case 'datetime':
+      case 'datetime-local':
+        return DateFormat::load('html_datetime')->getPattern();
+
+      default:
+        return $element['#date_date_format'];
+    }
+  }
+
+  /**
+   * Retrieves the right format for a HTML5 time element.
+   *
+   * The format is important because these elements will not work with any other
+   * format.
+   *
+   * @param string $element
+   *   The $element to assess.
+   *
+   * @return string
+   *   Returns the right format for the time element, or the original format
+   *   if this is not a HTML5 element.
+   */
+  protected static function getHtml5TimeFormat($element) {
+    switch ($element['#date_time_element']) {
+      case 'time':
+        return DateFormat::load('html_time')->getPattern();
+
+      default:
+        return $element['#date_time_format'];
+    }
+  }
+
+}
diff --git a/core/modules/system/src/Entity/DateFormat.php b/core/lib/Drupal/Core/Datetime/Entity/DateFormat.php
similarity index 74%
rename from core/modules/system/src/Entity/DateFormat.php
rename to core/lib/Drupal/Core/Datetime/Entity/DateFormat.php
index 67614d2513225ce7cc7691ee8dd2a6d1005773fb..ed40b900acbf8aea0946f6327bde6d85acfd6c7e 100644
--- a/core/modules/system/src/Entity/DateFormat.php
+++ b/core/lib/Drupal/Core/Datetime/Entity/DateFormat.php
@@ -2,15 +2,14 @@
 
 /**
  * @file
- * Contains \Drupal\system\Entity\DateFormat.
+ * Contains \Drupal\Core\Datetime\Entity\DateFormat.
  */
 
-namespace Drupal\system\Entity;
+namespace Drupal\Core\Datetime\Entity;
 
 use Drupal\Core\Config\Entity\ConfigEntityBase;
-use Drupal\Core\Datetime\DrupalDateTime;
 use Drupal\Core\Config\Entity\ConfigEntityInterface;
-use Drupal\system\DateFormatInterface;
+use Drupal\Core\Datetime\DateFormatInterface;
 
 /**
  * Defines the Date Format configuration entity class.
@@ -20,22 +19,12 @@
  *   label = @Translation("Date format"),
  *   handlers = {
  *     "access" = "Drupal\system\DateFormatAccessControlHandler",
- *     "list_builder" = "Drupal\system\DateFormatListBuilder",
- *     "form" = {
- *       "add" = "Drupal\system\Form\DateFormatAddForm",
- *       "edit" = "Drupal\system\Form\DateFormatEditForm",
- *       "delete" = "Drupal\system\Form\DateFormatDeleteForm"
- *     }
  *   },
  *   entity_keys = {
  *     "id" = "id",
  *     "label" = "label"
  *   },
  *   admin_permission = "administer site configuration",
- *   links = {
- *     "delete-form" = "entity.date_format.delete_form",
- *     "edit-form" = "entity.date_format.edit_form"
- *   }
  * )
  */
 class DateFormat extends ConfigEntityBase implements DateFormatInterface {
diff --git a/core/lib/Drupal/Core/Datetime/Plugin/README.txt b/core/lib/Drupal/Core/Datetime/Plugin/README.txt
new file mode 100644
index 0000000000000000000000000000000000000000..f761e0198cef57dbae101045b12e2bc350d208a9
--- /dev/null
+++ b/core/lib/Drupal/Core/Datetime/Plugin/README.txt
@@ -0,0 +1,4 @@
+@todo This must be here because DrupalKernel will only allow namespaces to
+  provide plugins if there is a Plugin subdirectory, and git does not allow
+  empty subdirectories. This file should be removed once
+  https://www.drupal.org/node/2309889 is fixed.
diff --git a/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php b/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php
index ab2a1f8f75068e17c7563bcbd81a4215fbd903dc..253af4876c5fc5c4011512c5a733b1783500dc30 100644
--- a/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php
+++ b/core/modules/config_translation/src/Tests/ConfigTranslationUiTest.php
@@ -397,15 +397,15 @@ public function testDateFormatTranslation() {
 
       // Update translatable fields.
       $edit = array(
-        'config_names[system.date_format.' . $id . '][label][translation]' => $id . ' - FR',
-        'config_names[system.date_format.' . $id . '][pattern][translation]' => 'D',
+        'config_names[core.date_format.' . $id . '][label][translation]' => $id . ' - FR',
+        'config_names[core.date_format.' . $id . '][pattern][translation]' => 'D',
       );
 
       // Save language specific version of form.
       $this->drupalPostForm($translation_page_url, $edit, t('Save translation'));
 
       // Get translation and check we've got the right value.
-      $override = \Drupal::languageManager()->getLanguageConfigOverride('fr', 'system.date_format.' . $id);
+      $override = \Drupal::languageManager()->getLanguageConfigOverride('fr', 'core.date_format.' . $id);
       $expected = array(
         'label' => $id . ' - FR',
         'pattern' => 'D',
diff --git a/core/modules/datetime/datetime.module b/core/modules/datetime/datetime.module
index 99cf2fd576a1469219c109f45bca9296779da73e..cca729f69b722c874168fce916975cb097282d13 100644
--- a/core/modules/datetime/datetime.module
+++ b/core/modules/datetime/datetime.module
@@ -10,7 +10,7 @@
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Template\Attribute;
-use Drupal\datetime\DateHelper;
+use Drupal\Core\Datetime\Element\Datetime;
 use Drupal\node\NodeInterface;
 
 /**
@@ -48,74 +48,6 @@ function datetime_help($route_name, RouteMatchInterface $route_match) {
     }
 }
 
-/**
- * Implements hook_element_info().
- */
-function datetime_element_info() {
-  $date_format = '';
-  $time_format = '';
-  // Date formats cannot be loaded during install or update.
-  if (!defined('MAINTENANCE_MODE')) {
-    if ($date_format_entity = entity_load('date_format', 'html_date')) {
-      $date_format = $date_format_entity->getPattern();
-    }
-    if ($time_format_entity = entity_load('date_format', 'html_time')) {
-      $time_format = $time_format_entity->getPattern();
-    }
-  }
-  $types['datetime'] = array(
-    '#input' => TRUE,
-    '#element_validate' => array('datetime_datetime_validate'),
-    '#process' => array(
-      'datetime_datetime_form_process',
-      array('Drupal\Core\Render\Element\RenderElement', 'processGroup'),
-    ),
-    '#pre_render' => array(
-      array('Drupal\Core\Render\Element\RenderElement', 'preRenderGroup'),
-    ),
-    '#theme' => 'datetime_form',
-    '#theme_wrappers' => array('datetime_wrapper'),
-    '#date_date_format' => $date_format,
-    '#date_date_element' => 'date',
-    '#date_date_callbacks' => array(),
-    '#date_time_format' => $time_format,
-    '#date_time_element' => 'time',
-    '#date_time_callbacks' => array(),
-    '#date_year_range' => '1900:2050',
-    '#date_increment' => 1,
-    '#date_timezone' => '',
-  );
-  $types['datelist'] = array(
-    '#input' => TRUE,
-    '#element_validate' => array('datetime_datelist_validate'),
-    '#process' => array('datetime_datelist_form_process'),
-    '#theme' => 'datetime_form',
-    '#theme_wrappers' => array('datetime_wrapper'),
-    '#date_part_order' => array('year', 'month', 'day', 'hour', 'minute'),
-    '#date_year_range' => '1900:2050',
-    '#date_increment' => 1,
-    '#date_date_callbacks' => array(),
-    '#date_timezone' => '',
-  );
-  return $types;
-}
-
-/**
- * Implements hook_theme().
- */
-function datetime_theme() {
-  return array(
-    'datetime_form' => array(
-      'template' => 'datetime-form',
-      'render element' => 'element',
-    ),
-    'datetime_wrapper' => array(
-      'template' => 'datetime-wrapper',
-      'render element' => 'element',
-    ),
-  );
-}
-
 /**
  * Validation callback for the datetime widget element.
  *
@@ -206,802 +138,6 @@ function datetime_date_default_time($date) {
   $date->setTime(12, 0, 0);
 }
 
-/**
- * Prepares variables for datetime form element templates.
- *
- * The datetime form element serves as a wrapper around the date element type,
- * which creates a date and a time component for a date.
- *
- * Default template: datetime-form.html.twig.
- *
- * @param array $variables
- *   An associative array containing:
- *   - element: An associative array containing the properties of the element.
- *     Properties used: #title, #value, #options, #description, #required,
- *     #attributes.
- *
- * @see form_process_datetime()
- */
-function template_preprocess_datetime_form(&$variables) {
-  $element = $variables['element'];
-
-  $variables['attributes'] = array();
-  if (isset($element['#id'])) {
-    $variables['attributes']['id'] = $element['#id'];
-  }
-  if (!empty($element['#attributes']['class'])) {
-    $variables['attributes']['class'] = (array) $element['#attributes']['class'];
-  }
-  $variables['attributes']['class'][] = 'container-inline';
-
-  $variables['content'] = $element;
-}
-
-/**
- * Prepares variables for datetime form wrapper templates.
- *
- * Default template: datetime-wrapper.html.twig.
- *
- * @param array $variables
- *   An associative array containing:
- *   - element: An associative array containing the properties of the element.
- *     Properties used: #title, #children, #required, #attributes.
- */
-function template_preprocess_datetime_wrapper(&$variables) {
-  $element = $variables['element'];
-
-  if (!empty($element['#title'])) {
-    $variables['title'] = $element['#title'];
-  }
-
-  if (!empty($element['#description'])) {
-    $variables['description'] = $element['#description'];
-  }
-
-  $title_attributes = array('class' => array('label'));
-  // For required datetime fields a 'form-required' class is appended to the
-  // label attributes.
-  if (!empty($element['#required'])) {
-    $title_attributes['class'][] = 'form-required';
-  }
-  $variables['title_attributes'] = new Attribute($title_attributes);
-  $variables['content'] = $element['#children'];
-}
-
-/**
- * Expands a datetime element type into date and/or time elements.
- *
- * All form elements are designed to have sane defaults so any or all can be
- * omitted. Both the date and time components are configurable so they can be
- * output as HTML5 datetime elements or not, as desired.
- *
- * Examples of possible configurations include:
- *   HTML5 date and time:
- *     #date_date_element = 'date';
- *     #date_time_element = 'time';
- *   HTML5 datetime:
- *     #date_date_element = 'datetime';
- *     #date_time_element = 'none';
- *   HTML5 time only:
- *     #date_date_element = 'none';
- *     #date_time_element = 'time'
- *   Non-HTML5:
- *     #date_date_element = 'text';
- *     #date_time_element = 'text';
- *
- * Required settings:
- *   - #default_value: A DrupalDateTime object, adjusted to the proper local
- *     timezone. Converting a date stored in the database from UTC to the local
- *     zone and converting it back to UTC before storing it is not handled here.
- *     This element accepts a date as the default value, and then converts the
- *     user input strings back into a new date object on submission. No timezone
- *     adjustment is performed.
- * Optional properties include:
- *   - #date_date_format: A date format string that describes the format that
- *     should be displayed to the end user for the date. When using HTML5
- *     elements the format MUST use the appropriate HTML5 format for that
- *     element, no other format will work. See the format_date() function for a
- *     list of the possible formats and HTML5 standards for the HTML5
- *     requirements. Defaults to the right HTML5 format for the chosen element
- *     if a HTML5 element is used, otherwise defaults to
- *     entity_load('date_format', 'html_date')->getPattern().
- *   - #date_date_element: The date element. Options are:
- *     - datetime: Use the HTML5 datetime element type.
- *     - datetime-local: Use the HTML5 datetime-local element type.
- *     - date: Use the HTML5 date element type.
- *     - text: No HTML5 element, use a normal text field.
- *     - none: Do not display a date element.
- *   - #date_date_callbacks: Array of optional callbacks for the date element.
- *     Can be used to add a jQuery datepicker.
- *   - #date_time_element: The time element. Options are:
- *     - time: Use a HTML5 time element type.
- *     - text: No HTML5 element, use a normal text field.
- *     - none: Do not display a time element.
- *   - #date_time_format: A date format string that describes the format that
- *     should be displayed to the end user for the time. When using HTML5
- *     elements the format MUST use the appropriate HTML5 format for that
- *     element, no other format will work. See the format_date() function for
- *     a list of the possible formats and HTML5 standards for the HTML5
- *     requirements. Defaults to the right HTML5 format for the chosen element
- *     if a HTML5 element is used, otherwise defaults to
- *     entity_load('date_format', 'html_time')->getPattern().
- *   - #date_time_callbacks: An array of optional callbacks for the time
- *     element. Can be used to add a jQuery timepicker or an 'All day' checkbox.
- *   - #date_year_range: A description of the range of years to allow, like
- *     '1900:2050', '-3:+3' or '2000:+3', where the first value describes the
- *     earliest year and the second the latest year in the range. A year
- *     in either position means that specific year. A +/- value describes a
- *     dynamic value that is that many years earlier or later than the current
- *     year at the time the form is displayed. Used in jQueryUI datepicker year
- *     range and HTML5 min/max date settings. Defaults to '1900:2050'.
- *   - #date_increment: The increment to use for minutes and seconds, i.e.
- *    '15' would show only :00, :15, :30 and :45. Used for HTML5 step values and
- *     jQueryUI datepicker settings. Defaults to 1 to show every minute.
- *   - #date_timezone: The local timezone to use when creating dates. Generally
- *     this should be left empty and it will be set correctly for the user using
- *     the form. Useful if the default value is empty to designate a desired
- *     timezone for dates created in form processing. If a default date is
- *     provided, this value will be ignored, the timezone in the default date
- *     takes precedence. Defaults to the value returned by
- *     drupal_get_user_timezone().
- *
- * Example usage:
- * @code
- *   $form = array(
- *     '#type' => 'datetime',
- *     '#default_value' => new DrupalDateTime('2000-01-01 00:00:00'),
- *     '#date_date_element' => 'date',
- *     '#date_time_element' => 'none',
- *     '#date_year_range' => '2010:+3',
- *   );
- * @endcode
- *
- * @param array $element
- *   The form element whose value is being processed.
- * @param \Drupal\Core\Form\FormStateInterface $form_state
- *   The current state of the form.
- *
- * @return array
- *   The form element whose value has been processed.
- */
-function datetime_datetime_form_process($element, FormStateInterface $form_state) {
-  $format_settings = array();
-  // The value callback has populated the #value array.
-  $date = !empty($element['#value']['object']) ? $element['#value']['object'] : NULL;
-
-  // Set a fallback timezone.
-  if ($date instanceOf DrupalDateTime) {
-    $element['#date_timezone'] = $date->getTimezone()->getName();
-  }
-  elseif (!empty($element['#timezone'])) {
-    $element['#date_timezone'] = $element['#date_timezone'];
-  }
-  else {
-    $element['#date_timezone'] = drupal_get_user_timezone();
-  }
-
-  $element['#tree'] = TRUE;
-
-  if ($element['#date_date_element'] != 'none') {
-
-    $date_format = $element['#date_date_element'] != 'none' ? datetime_html5_format('date', $element) : '';
-    $date_value = !empty($date) ? $date->format($date_format, $format_settings) : $element['#value']['date'];
-
-    // Creating format examples on every individual date item is messy, and
-    // placeholders are invalid for HTML5 date and datetime, so an example
-    // format is appended to the title to appear in tooltips.
-    $extra_attributes = array(
-      'title' => t('Date (i.e. !format)', array('!format' => datetime_format_example($date_format))),
-      'type' => $element['#date_date_element'],
-    );
-
-    // Adds the HTML5 date attributes.
-    if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
-      $html5_min = clone($date);
-      $range = datetime_range_years($element['#date_year_range'], $date);
-      $html5_min->setDate($range[0], 1, 1)->setTime(0, 0, 0);
-      $html5_max = clone($date);
-      $html5_max->setDate($range[1], 12, 31)->setTime(23, 59, 59);
-
-      $extra_attributes += array(
-        'min' => $html5_min->format($date_format, $format_settings),
-        'max' => $html5_max->format($date_format, $format_settings),
-      );
-    }
-
-    $element['date'] = array(
-      '#type' => 'date',
-      '#title' => t('Date'),
-      '#title_display' => 'invisible',
-      '#value' => $date_value,
-      '#attributes' => $element['#attributes'] + $extra_attributes,
-      '#required' => $element['#required'],
-      '#size' => max(12, strlen($element['#value']['date'])),
-    );
-
-    // Allows custom callbacks to alter the element.
-    if (!empty($element['#date_date_callbacks'])) {
-      foreach ($element['#date_date_callbacks'] as $callback) {
-        if (function_exists($callback)) {
-          $callback($element, $form_state, $date);
-        }
-      }
-    }
-  }
-
-  if ($element['#date_time_element'] != 'none') {
-
-    $time_format = $element['#date_time_element'] != 'none' ? datetime_html5_format('time', $element) : '';
-    $time_value = !empty($date) ? $date->format($time_format, $format_settings) : $element['#value']['time'];
-
-    // Adds the HTML5 attributes.
-    $extra_attributes = array(
-      'title' =>t('Time (i.e. !format)', array('!format' => datetime_format_example($time_format))),
-      'type' => $element['#date_time_element'],
-      'step' => $element['#date_increment'],
-    );
-    $element['time'] = array(
-      '#type' => 'date',
-      '#title' => t('Time'),
-      '#title_display' => 'invisible',
-      '#value' => $time_value,
-      '#attributes' => $element['#attributes'] + $extra_attributes,
-      '#required' => $element['#required'],
-      '#size' => 12,
-    );
-
-    // Allows custom callbacks to alter the element.
-    if (!empty($element['#date_time_callbacks'])) {
-      foreach ($element['#date_time_callbacks'] as $callback) {
-        if (function_exists($callback)) {
-          $callback($element, $form_state, $date);
-        }
-      }
-    }
-  }
-
-  return $element;
-}
-
-/**
- * Value callback for a datetime element.
- *
- * @param array $element
- *   The form element whose value is being populated.
- * @param array $input
- *   (optional) The incoming input to populate the form element. If this is
- *   FALSE, the element's default value should be returned. Defaults to FALSE.
- *
- * @return array
- *   The data that will appear in the $element_state['values'] collection for
- *   this element. Return nothing to use the default.
- */
-function form_type_datetime_value($element, $input = FALSE) {
-  if ($input !== FALSE) {
-    $date_input  = $element['#date_date_element'] != 'none' && !empty($input['date']) ? $input['date'] : '';
-    $time_input  = $element['#date_time_element'] != 'none' && !empty($input['time']) ? $input['time'] : '';
-    $date_format = $element['#date_date_element'] != 'none' ? datetime_html5_format('date', $element) : '';
-    $time_format = $element['#date_time_element'] != 'none' ? datetime_html5_format('time', $element) : '';
-    $timezone = !empty($element['#date_timezone']) ? $element['#date_timezone'] : NULL;
-
-    // Seconds will be omitted in a post in case there's no entry.
-    if (!empty($time_input) && strlen($time_input) == 5) {
-      $time_input .= ':00';
-    }
-
-    try {
-      $date_time_format = trim($date_format . ' ' . $time_format);
-      $date_time_input = trim($date_input . ' ' . $time_input);
-      $date = DrupalDateTime::createFromFormat($date_time_format, $date_time_input, $timezone);
-    }
-    catch (\Exception $e) {
-      $date = NULL;
-    }
-    $input = array(
-      'date'   => $date_input,
-      'time'   => $time_input,
-      'object' => $date,
-    );
-  }
-  else {
-    $date = $element['#default_value'];
-    if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
-      $input = array(
-        'date'   => $date->format($element['#date_date_format']),
-        'time'   => $date->format($element['#date_time_format']),
-        'object' => $date,
-      );
-    }
-    else {
-      $input = array(
-        'date'   => '',
-        'time'   => '',
-        'object' => NULL,
-      );
-    }
-  }
-  return $input;
-}
-
-/**
- * Validation callback for a datetime element.
- *
- * If the date is valid, the date object created from the user input is set in
- * the form for use by the caller. The work of compiling the user input back
- * into a date object is handled by the value callback, so we can use it here.
- * We also have the raw input available for validation testing.
- *
- * @param array $element
- *   The form element whose value is being validated.
- * @param \Drupal\Core\Form\FormStateInterface $form_state
- *   The current state of the form.
- */
-function datetime_datetime_validate($element, FormStateInterface $form_state) {
-
-  $input_exists = FALSE;
-  $input = NestedArray::getValue($form_state->getValues(), $element['#parents'], $input_exists);
-  if ($input_exists) {
-
-    $title = !empty($element['#title']) ? $element['#title'] : '';
-    $date_format = $element['#date_date_element'] != 'none' ? datetime_html5_format('date', $element) : '';
-    $time_format = $element['#date_time_element'] != 'none' ? datetime_html5_format('time', $element) : '';
-    $format = trim($date_format . ' ' . $time_format);
-
-    // If there's empty input and the field is not required, set it to empty.
-    if (empty($input['date']) && empty($input['time']) && !$element['#required']) {
-      form_set_value($element, NULL, $form_state);
-    }
-    // If there's empty input and the field is required, set an error. A
-    // reminder of the required format in the message provides a good UX.
-    elseif (empty($input['date']) && empty($input['time']) && $element['#required']) {
-      form_error($element, $form_state, t('The %field date is required. Please enter a date in the format %format.', array('%field' => $title, '%format' => datetime_format_example($format))));
-    }
-    else {
-      // If the date is valid, set it.
-      $date = $input['object'];
-      if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
-        form_set_value($element, $date, $form_state);
-      }
-      // If the date is invalid, set an error. A reminder of the required
-      // format in the message provides a good UX.
-      else {
-        form_error($element, $form_state, t('The %field date is invalid. Please enter a date in the format %format.', array('%field' => $title, '%format' => datetime_format_example($format))));
-      }
-    }
-  }
-}
-
-/**
- * Retrieves the right format for a HTML5 date element.
- *
- * The format is important because these elements will not work with any other
- * format.
- *
- * @param string $part
- *   The type of element format to retrieve.
- * @param string $element
- *   The $element to assess.
- *
- * @return string
- *   Returns the right format for the type of element, or the original format
- *   if this is not a HTML5 element.
- */
-function datetime_html5_format($part, $element) {
-  switch ($part) {
-    case 'date':
-      switch ($element['#date_date_element']) {
-        case 'date':
-          return entity_load('date_format', 'html_date')->getPattern();
-
-        case 'datetime':
-        case 'datetime-local':
-          return entity_load('date_format', 'html_datetime')->getPattern();
-
-        default:
-          return $element['#date_date_format'];
-      }
-      break;
-
-    case 'time':
-      switch ($element['#date_time_element']) {
-        case 'time':
-          return entity_load('date_format', 'html_time')->getPattern();
-
-        default:
-          return $element['#date_time_format'];
-      }
-      break;
-  }
-}
-
-/**
- * Creates an example for a date format.
- *
- * This is centralized for a consistent method of creating these examples.
- *
- * @param string $format
- *
- *
- * @return string
- *
- */
-function datetime_format_example($format) {
-  $date = &drupal_static(__FUNCTION__);
-  if (empty($date)) {
-    $date = new DrupalDateTime();
-  }
-  return $date->format($format);
-}
-
-/**
- * Expands a date element into an array of individual elements.
- *
- * Required settings:
- *   - #default_value: A DrupalDateTime object, adjusted to the proper local
- *     timezone. Converting a date stored in the database from UTC to the local
- *     zone and converting it back to UTC before storing it is not handled here.
- *     This element accepts a date as the default value, and then converts the
- *     user input strings back into a new date object on submission. No timezone
- *     adjustment is performed.
- * Optional properties include:
- *   - #date_part_order: Array of date parts indicating the parts and order
- *     that should be used in the selector, optionally including 'ampm' for
- *     12 hour time. Default is array('year', 'month', 'day', 'hour', 'minute').
- *   - #date_text_parts: Array of date parts that should be presented as
- *     text fields instead of drop-down selectors. Default is an empty array.
- *   - #date_date_callbacks: Array of optional callbacks for the date element.
- *   - #date_year_range: A description of the range of years to allow, like
- *     '1900:2050', '-3:+3' or '2000:+3', where the first value describes the
- *     earliest year and the second the latest year in the range. A year
- *     in either position means that specific year. A +/- value describes a
- *     dynamic value that is that many years earlier or later than the current
- *     year at the time the form is displayed. Defaults to '1900:2050'.
- *   - #date_increment: The increment to use for minutes and seconds, i.e.
- *     '15' would show only :00, :15, :30 and :45. Defaults to 1 to show every
- *     minute.
- *   - #date_timezone: The local timezone to use when creating dates. Generally
- *     this should be left empty and it will be set correctly for the user using
- *     the form. Useful if the default value is empty to designate a desired
- *     timezone for dates created in form processing. If a default date is
- *     provided, this value will be ignored, the timezone in the default date
- *     takes precedence. Defaults to the value returned by
- *     drupal_get_user_timezone().
- *
- * Example usage:
- * @code
- *   $form = array(
- *     '#type' => 'datelist',
- *     '#default_value' => new DrupalDateTime('2000-01-01 00:00:00'),
- *     '#date_part_order' => array('month', 'day', 'year', 'hour', 'minute', 'ampm'),
- *     '#date_text_parts' => array('year'),
- *     '#date_year_range' => '2010:2020',
- *     '#date_increment' => 15,
- *   );
- * @endcode
- *
- * @param array $element
- *   The form element whose value is being processed.
- * @param \Drupal\Core\Form\FormStateInterface $form_state
- *   The current state of the form.
- */
-function datetime_datelist_form_process($element, FormStateInterface $form_state) {
-
-  // Load translated date part labels from the appropriate calendar plugin.
-  $date_helper = new DateHelper();
-
-  // The value callback has populated the #value array.
-  $date = !empty($element['#value']['object']) ? $element['#value']['object'] : NULL;
-
-  // Set a fallback timezone.
-  if ($date instanceOf DrupalDateTime) {
-    $element['#date_timezone'] = $date->getTimezone()->getName();
-  }
-  elseif (!empty($element['#timezone'])) {
-    $element['#date_timezone'] = $element['#date_timezone'];
-  }
-  else {
-    $element['#date_timezone'] = drupal_get_user_timezone();
-  }
-
-  $element['#tree'] = TRUE;
-
-  // Determine the order of the date elements.
-  $order = !empty($element['#date_part_order']) ? $element['#date_part_order'] : array('year', 'month', 'day');
-  $text_parts = !empty($element['#date_text_parts']) ? $element['#date_text_parts'] : array();
-
-  // Output multi-selector for date.
-  foreach ($order as $part) {
-    switch ($part) {
-      case 'day':
-        $options = $date_helper->days($element['#required']);
-        $format = 'j';
-        $title = t('Day');
-        break;
-
-      case 'month':
-        $options = $date_helper->monthNamesAbbr($element['#required']);
-        $format = 'n';
-        $title = t('Month');
-        break;
-
-      case 'year':
-        $range = datetime_range_years($element['#date_year_range'], $date);
-        $options = $date_helper->years($range[0], $range[1], $element['#required']);
-        $format = 'Y';
-        $title = t('Year');
-        break;
-
-      case 'hour':
-        $format = in_array('ampm', $element['#date_part_order']) ? 'g': 'G';
-        $options = $date_helper->hours($format, $element['#required']);
-        $title = t('Hour');
-        break;
-
-      case 'minute':
-        $format = 'i';
-        $options = $date_helper->minutes($format, $element['#required'], $element['#date_increment']);
-        $title = t('Minute');
-        break;
-
-      case 'second':
-        $format = 's';
-        $options = $date_helper->seconds($format, $element['#required'], $element['#date_increment']);
-        $title = t('Second');
-        break;
-
-      case 'ampm':
-        $format = 'a';
-        $options = $date_helper->ampm($element['#required']);
-        $title = t('AM/PM');
-    }
-
-    $default = !empty($element['#value'][$part]) ? $element['#value'][$part] : '';
-    $value = $date instanceOf DrupalDateTime && !$date->hasErrors() ? $date->format($format) : $default;
-    if (!empty($value) && $part != 'ampm') {
-      $value = intval($value);
-    }
-
-    $element['#attributes']['title'] = $title;
-    $element[$part] = array(
-      '#type' => in_array($part, $text_parts) ? 'textfield' : 'select',
-      '#title' => $title,
-      '#title_display' => 'invisible',
-      '#value' => $value,
-      '#attributes' => $element['#attributes'],
-      '#options' => $options,
-      '#required' => $element['#required'],
-    );
-  }
-
-  // Allows custom callbacks to alter the element.
-  if (!empty($element['#date_date_callbacks'])) {
-    foreach ($element['#date_date_callbacks'] as $callback) {
-      if (function_exists($callback)) {
-        $callback($element, $form_state, $date);
-      }
-    }
-  }
-
-  return $element;
-}
-
-/**
- * Element value callback for datelist element.
- *
- * Validates the date type to adjust 12 hour time and prevent invalid dates. If
- * the date is valid, the date is set in the form.
- *
- * @param array $element
- *   The element being processed.
- * @param array|false $input
- * @param \Drupal\Core\Form\FormStateInterface $form_state
- *   The current state of the form.
- *
- * @return array
- */
-function form_type_datelist_value($element, $input = FALSE, FormStateInterface $form_state) {
-  $parts = $element['#date_part_order'];
-  $increment = $element['#date_increment'];
-
-  $date = NULL;
-  if ($input !== FALSE) {
-    $return = $input;
-    if (isset($input['ampm'])) {
-      if ($input['ampm'] == 'pm' && $input['hour'] < 12) {
-        $input['hour'] += 12;
-      }
-      elseif ($input['ampm'] == 'am' && $input['hour'] == 12) {
-        $input['hour'] -= 12;
-      }
-      unset($input['ampm']);
-    }
-    $timezone = !empty($element['#date_timezone']) ? $element['#date_timezone'] : NULL;
-    $date = DrupalDateTime::createFromArray($input, $timezone);
-    if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
-      date_increment_round($date, $increment);
-    }
-  }
-  else {
-    $return = array_fill_keys($parts, '');
-    if (!empty($element['#default_value'])) {
-      $date = $element['#default_value'];
-      if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
-        date_increment_round($date, $increment);
-        foreach ($parts as $part) {
-          switch ($part) {
-            case 'day':
-              $format = 'j';
-              break;
-
-            case 'month':
-              $format = 'n';
-              break;
-
-            case 'year':
-              $format = 'Y';
-              break;
-
-            case 'hour':
-              $format = in_array('ampm', $element['#date_part_order']) ? 'g': 'G';
-              break;
-
-            case 'minute':
-              $format = 'i';
-              break;
-
-            case 'second':
-              $format = 's';
-              break;
-
-            case 'ampm':
-              $format = 'a';
-          }
-          $return[$part] = $date->format($format);
-        }
-      }
-    }
-  }
-  $return['object'] = $date;
-  return $return;
-}
-
-/**
- * Validation callback for a datelist element.
- *
- * If the date is valid, the date object created from the user input is set in
- * the form for use by the caller. The work of compiling the user input back
- * into a date object is handled by the value callback, so we can use it here.
- * We also have the raw input available for validation testing.
- *
- * @param array $element
- *   The element being processed.
- * @param \Drupal\Core\Form\FormStateInterface $form_state
- *   The current state of the form.
- */
-function datetime_datelist_validate($element, FormStateInterface $form_state) {
-  $input_exists = FALSE;
-  $input = NestedArray::getValue($form_state->getValues(), $element['#parents'], $input_exists);
-  if ($input_exists) {
-
-    // If there's empty input and the field is not required, set it to empty.
-    if (empty($input['year']) && empty($input['month']) && empty($input['day']) && !$element['#required']) {
-      form_set_value($element, NULL, $form_state);
-    }
-    // If there's empty input and the field is required, set an error.
-    elseif (empty($input['year']) && empty($input['month']) && empty($input['day']) && $element['#required']) {
-      form_error($element, $form_state, t('The %field date is required.'));
-    }
-    else {
-      // If the input is valid, set it.
-      $date = $input['object'];
-      if ($date instanceOf DrupalDateTime && !$date->hasErrors()) {
-        form_set_value($element, $date, $form_state);
-      }
-      // If the input is invalid, set an error.
-      else {
-        form_error($element, $form_state, t('The %field date is invalid.'));
-      }
-    }
-  }
-}
-
-/**
- * Rounds minutes and seconds to nearest requested value.
- *
- * @param $date
- *
- * @param $increment
- *
- *
- * @return
- *
- */
-function date_increment_round(&$date, $increment) {
-  // Round minutes and seconds, if necessary.
-  if ($date instanceOf DrupalDateTime && $increment > 1) {
-    $day = intval(date_format($date, 'j'));
-    $hour = intval(date_format($date, 'H'));
-    $second = intval(round(intval(date_format($date, 's')) / $increment) * $increment);
-    $minute = intval(date_format($date, 'i'));
-    if ($second == 60) {
-      $minute += 1;
-      $second = 0;
-    }
-    $minute = intval(round($minute / $increment) * $increment);
-    if ($minute == 60) {
-      $hour += 1;
-      $minute = 0;
-    }
-    date_time_set($date, $hour, $minute, $second);
-    if ($hour == 24) {
-      $day += 1;
-      $year = date_format($date, 'Y');
-      $month = date_format($date, 'n');
-      date_date_set($date, $year, $month, $day);
-    }
-  }
-  return $date;
-}
-
-/**
- * Specifies the start and end year to use as a date range.
- *
- * Handles a string like -3:+3 or 2001:2010 to describe a dynamic range of
- * minimum and maximum years to use in a date selector.
- *
- * Centers the range around the current year, if any, but expands it far enough
- * so it will pick up the year value in the field in case the value in the field
- * is outside the initial range.
- *
- * @param string $string
- *   A min and max year string like '-3:+1' or '2000:2010' or '2000:+3'.
- * @param object $date
- *   (optional) A date object to test as a default value. Defaults to NULL.
- *
- * @return array
- *   A numerically indexed array, containing the minimum and maximum year
- *   described by this pattern.
- */
-function datetime_range_years($string, $date = NULL) {
-
-  $this_year = date_format(new DrupalDateTime(), 'Y');
-  list($min_year, $max_year) = explode(':', $string);
-
-  // Valid patterns would be -5:+5, 0:+1, 2008:2010.
-  $plus_pattern = '@[\+|\-][0-9]{1,4}@';
-  $year_pattern = '@^[0-9]{4}@';
-  if (!preg_match($year_pattern, $min_year, $matches)) {
-    if (preg_match($plus_pattern, $min_year, $matches)) {
-      $min_year = $this_year + $matches[0];
-    }
-    else {
-      $min_year = $this_year;
-    }
-  }
-  if (!preg_match($year_pattern, $max_year, $matches)) {
-    if (preg_match($plus_pattern, $max_year, $matches)) {
-      $max_year = $this_year + $matches[0];
-    }
-    else {
-      $max_year = $this_year;
-    }
-  }
-  // We expect the $min year to be less than the $max year. Some custom values
-  // for -99:+99 might not obey that.
-  if ($min_year > $max_year) {
-    $temp = $max_year;
-    $max_year = $min_year;
-    $min_year = $temp;
-  }
-  // If there is a current value, stretch the range to include it.
-  $value_year = $date instanceOf DrupalDateTime ? $date->format('Y') : '';
-  if (!empty($value_year)) {
-    $min_year = min($value_year, $min_year);
-    $max_year = max($value_year, $max_year);
-  }
-  return array($min_year, $max_year);
-}
-
 /**
  * Implements hook_form_BASE_FORM_ID_alter() for node forms.
  */
@@ -1010,7 +146,7 @@ function datetime_form_node_form_alter(&$form, FormStateInterface $form_state, $
   $form['created']['#type'] = 'datetime';
   $date_format = entity_load('date_format', 'html_date')->getPattern();
   $time_format = entity_load('date_format', 'html_time')->getPattern();
-  $form['created']['#description'] = t('Format: %format. Leave blank to use the time of form submission.', array('%format' => datetime_format_example($date_format . ' ' . $time_format)));
+  $form['created']['#description'] = t('Format: %format. Leave blank to use the time of form submission.', array('%format' => Datetime::formatExample($date_format . ' ' . $time_format)));
   unset($form['created']['#maxlength']);
 }
 
diff --git a/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php b/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php
index bd30b441009e2ddc217af561e281ccb3972f3de8..ac6bb2d7f8ceae2a36bee8fb522ed920c06d1dc3 100644
--- a/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php
+++ b/core/modules/locale/src/Tests/LocaleConfigTranslationTest.php
@@ -110,7 +110,7 @@ public function testConfigTranslation() {
     );
     $this->drupalPostForm('admin/config/regional/translate', $edit, t('Save translations'));
 
-    $wrapper = $this->container->get('locale.config.typed')->get('system.date_format.medium');
+    $wrapper = $this->container->get('locale.config.typed')->get('core.date_format.medium');
 
     // Get translation and check we've only got the site name.
     $translation = $wrapper->getTranslation($langcode);
diff --git a/core/modules/migrate/src/Plugin/migrate/destination/EntityDateFormat.php b/core/modules/migrate/src/Plugin/migrate/destination/EntityDateFormat.php
index 695056aa924348fae479a1fb847a849acc541a96..d471ceac0b33a4276ba7fd1967592f1b51af523f 100644
--- a/core/modules/migrate/src/Plugin/migrate/destination/EntityDateFormat.php
+++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityDateFormat.php
@@ -19,7 +19,7 @@ class EntityDateFormat extends EntityConfigBase {
   /**
    * {@inheritdoc}
    *
-   * @param \Drupal\system\DateFormatInterface $entity
+   * @param \Drupal\Core\Datetime\DateFormatInterface $entity
    *   The date entity.
    */
   protected function updateEntityProperty(EntityInterface $entity, array $parents, $value) {
diff --git a/core/modules/migrate_drupal/src/Tests/d6/MigrateDateFormatTest.php b/core/modules/migrate_drupal/src/Tests/d6/MigrateDateFormatTest.php
index b1ec34be709ca84d066308526586ae09ff9e2d16..247f00f8909b5a79e0cd5ecd8e16c3e673522a0d 100644
--- a/core/modules/migrate_drupal/src/Tests/d6/MigrateDateFormatTest.php
+++ b/core/modules/migrate_drupal/src/Tests/d6/MigrateDateFormatTest.php
@@ -9,7 +9,7 @@
 use Drupal\Core\Database\Database;
 
 /**
- * Upgrade date formats to system.date_format.*.yml.
+ * Upgrade date formats to core.date_format.*.yml.
  *
  * @group migrate_drupal
  */
diff --git a/core/modules/system/config/install/system.date_format.fallback.yml b/core/modules/system/config/install/core.date_format.fallback.yml
similarity index 100%
rename from core/modules/system/config/install/system.date_format.fallback.yml
rename to core/modules/system/config/install/core.date_format.fallback.yml
diff --git a/core/modules/system/config/install/system.date_format.html_date.yml b/core/modules/system/config/install/core.date_format.html_date.yml
similarity index 100%
rename from core/modules/system/config/install/system.date_format.html_date.yml
rename to core/modules/system/config/install/core.date_format.html_date.yml
diff --git a/core/modules/system/config/install/system.date_format.html_datetime.yml b/core/modules/system/config/install/core.date_format.html_datetime.yml
similarity index 100%
rename from core/modules/system/config/install/system.date_format.html_datetime.yml
rename to core/modules/system/config/install/core.date_format.html_datetime.yml
diff --git a/core/modules/system/config/install/system.date_format.html_month.yml b/core/modules/system/config/install/core.date_format.html_month.yml
similarity index 100%
rename from core/modules/system/config/install/system.date_format.html_month.yml
rename to core/modules/system/config/install/core.date_format.html_month.yml
diff --git a/core/modules/system/config/install/system.date_format.html_time.yml b/core/modules/system/config/install/core.date_format.html_time.yml
similarity index 100%
rename from core/modules/system/config/install/system.date_format.html_time.yml
rename to core/modules/system/config/install/core.date_format.html_time.yml
diff --git a/core/modules/system/config/install/system.date_format.html_week.yml b/core/modules/system/config/install/core.date_format.html_week.yml
similarity index 100%
rename from core/modules/system/config/install/system.date_format.html_week.yml
rename to core/modules/system/config/install/core.date_format.html_week.yml
diff --git a/core/modules/system/config/install/system.date_format.html_year.yml b/core/modules/system/config/install/core.date_format.html_year.yml
similarity index 100%
rename from core/modules/system/config/install/system.date_format.html_year.yml
rename to core/modules/system/config/install/core.date_format.html_year.yml
diff --git a/core/modules/system/config/install/system.date_format.html_yearless_date.yml b/core/modules/system/config/install/core.date_format.html_yearless_date.yml
similarity index 100%
rename from core/modules/system/config/install/system.date_format.html_yearless_date.yml
rename to core/modules/system/config/install/core.date_format.html_yearless_date.yml
diff --git a/core/modules/system/config/install/system.date_format.long.yml b/core/modules/system/config/install/core.date_format.long.yml
similarity index 100%
rename from core/modules/system/config/install/system.date_format.long.yml
rename to core/modules/system/config/install/core.date_format.long.yml
diff --git a/core/modules/system/config/install/system.date_format.medium.yml b/core/modules/system/config/install/core.date_format.medium.yml
similarity index 100%
rename from core/modules/system/config/install/system.date_format.medium.yml
rename to core/modules/system/config/install/core.date_format.medium.yml
diff --git a/core/modules/system/config/install/system.date_format.short.yml b/core/modules/system/config/install/core.date_format.short.yml
similarity index 100%
rename from core/modules/system/config/install/system.date_format.short.yml
rename to core/modules/system/config/install/core.date_format.short.yml
diff --git a/core/modules/system/config/schema/system.schema.yml b/core/modules/system/config/schema/system.schema.yml
index 2e9219d42b1bb1a42cbf237bd563bc3b713d578e..3f5ecbcace14c06fec9a55b8318c4bcdf36bd89b 100644
--- a/core/modules/system/config/schema/system.schema.yml
+++ b/core/modules/system/config/schema/system.schema.yml
@@ -114,26 +114,6 @@ system.date:
               type: boolean
               label: 'Remind users at login if their time zone is not set'
 
-system.date_format.*:
-  type: config_entity
-  label: 'Date format'
-  mapping:
-    id:
-      type: string
-      label: 'ID'
-    label:
-      type: label
-      label: 'Label'
-    locked:
-      type: boolean
-      label: 'Locked'
-    pattern:
-      type: date_format
-      label: 'PHP date format'
-    langcode:
-      type: string
-      label: 'Default language'
-
 system.diff:
   type: mapping
   label: 'Diff settings'
diff --git a/core/modules/system/src/Tests/Extension/ThemeHandlerTest.php b/core/modules/system/src/Tests/Extension/ThemeHandlerTest.php
index 75d10fd9f1619159b86a4c5c8e855c5643eb1303..f652bfdc495b87ad6663b185a7fc8e69f03ccdf1 100644
--- a/core/modules/system/src/Tests/Extension/ThemeHandlerTest.php
+++ b/core/modules/system/src/Tests/Extension/ThemeHandlerTest.php
@@ -186,14 +186,14 @@ function testDisableEnable() {
     $this->themeHandler()->disable(array($name));
 
     $this->assertIdentical($this->config("$name.settings")->get('base'), 'only');
-    $this->assertIdentical($this->config('system.date_format.fancy')->get('label'), 'Fancy date');
+    $this->assertIdentical($this->config('core.date_format.fancy')->get('label'), 'Fancy date');
 
     // Default configuration never overwrites custom configuration, so just
     // changing values in existing configuration will cause ConfigInstaller to
     // simply skip those files. To ensure that no default configuration is
     // re-imported, the custom configuration has to be deleted.
     $this->configStorage()->delete("$name.settings");
-    $this->configStorage()->delete('system.date_format.fancy');
+    $this->configStorage()->delete('core.date_format.fancy');
     // Reflect direct storage operations in ConfigFactory.
     $this->container->get('config.factory')->reset();
 
@@ -206,7 +206,7 @@ function testDisableEnable() {
     $this->assertEqual(array_keys(system_list('theme')), array_keys($themes));
 
     $this->assertFalse($this->config("$name.settings")->get());
-    $this->assertNull($this->config('system.date_format.fancy')->get('label'));
+    $this->assertNull($this->config('core.date_format.fancy')->get('label'));
   }
 
   /**
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 4c6bc602e23742e2f8ea049ac2c21a9be5d2c821..f7af74f89211cd30b0f0f8436859a33eab6145f8 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -1277,6 +1277,20 @@ function system_retrieve_file($url, $destination = NULL, $managed = FALSE, $repl
   return $local;
 }
 
+/**
+ * Implements hook_entity_type_build().
+ */
+function system_entity_type_build(array &$entity_types) {
+  /** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */
+  $entity_types['date_format']
+    ->setFormClass('add', 'Drupal\system\Form\DateFormatAddForm')
+    ->setFormClass('edit', 'Drupal\system\Form\DateFormatEditForm')
+    ->setFormClass('delete', 'Drupal\system\Form\DateFormatDeleteForm')
+    ->setListBuilderClass('Drupal\system\DateFormatListBuilder')
+    ->setLinkTemplate('edit-form', 'entity.date_format.edit_form')
+    ->setLinkTemplate('delete-form', 'entity.date_format.delete_form');
+}
+
 /**
  * Implements hook_page_alter().
  */
diff --git a/core/modules/datetime/templates/datetime-form.html.twig b/core/modules/system/templates/datetime-form.html.twig
similarity index 100%
rename from core/modules/datetime/templates/datetime-form.html.twig
rename to core/modules/system/templates/datetime-form.html.twig
diff --git a/core/modules/datetime/templates/datetime-wrapper.html.twig b/core/modules/system/templates/datetime-wrapper.html.twig
similarity index 100%
rename from core/modules/datetime/templates/datetime-wrapper.html.twig
rename to core/modules/system/templates/datetime-wrapper.html.twig
diff --git a/core/modules/system/tests/themes/test_basetheme/config/install/system.date_format.fancy.yml b/core/modules/system/tests/themes/test_basetheme/config/install/core.date_format.fancy.yml
similarity index 100%
rename from core/modules/system/tests/themes/test_basetheme/config/install/system.date_format.fancy.yml
rename to core/modules/system/tests/themes/test_basetheme/config/install/core.date_format.fancy.yml