diff --git a/core/composer.json b/core/composer.json
index 53ab8fa..862b5a3 100644
--- a/core/composer.json
+++ b/core/composer.json
@@ -94,6 +94,7 @@
         "drupal/editor": "self.version",
         "drupal/entity_reference": "self.version",
         "drupal/field": "self.version",
+        "drupal/field_layout": "self.version",
         "drupal/field_ui": "self.version",
         "drupal/file": "self.version",
         "drupal/filter": "self.version",
diff --git a/core/lib/Drupal/Component/Render/FormattableMarkup.php b/core/lib/Drupal/Component/Render/FormattableMarkup.php
index d9fbf2f..e5637e3 100644
--- a/core/lib/Drupal/Component/Render/FormattableMarkup.php
+++ b/core/lib/Drupal/Component/Render/FormattableMarkup.php
@@ -227,11 +227,16 @@ protected static function placeholderFormat($string, array $args) {
         default:
           // We do not trigger an error for placeholder that start with an
           // alphabetic character.
+          // @todo https://www.drupal.org/node/2807743 Change to an exception
+          //   and always throw regardless of the first character.
           if (!ctype_alpha($key[0])) {
             // We trigger an error as we may want to introduce new placeholders
             // in the future without breaking backward compatibility.
             trigger_error('Invalid placeholder (' . $key . ') in string: ' . $string, E_USER_ERROR);
           }
+          // If the placeholder is not a recognised placeholder ensure non-safe
+          // values are escaped.
+          $args[$key] = '<em class="placeholder">' . static::placeholderEscape($value) . '</em>';
           break;
       }
     }
diff --git a/core/modules/field_layout/config/schema/field_layout.schema.yml b/core/modules/field_layout/config/schema/field_layout.schema.yml
new file mode 100644
index 0000000..a490d6b
--- /dev/null
+++ b/core/modules/field_layout/config/schema/field_layout.schema.yml
@@ -0,0 +1,16 @@
+core.entity_view_display.*.*.*.third_party.field_layout:
+  type: field_layout.third_party_settings
+
+core.entity_form_display.*.*.*.third_party.field_layout:
+  type: field_layout.third_party_settings
+
+theme_settings.third_party.field_layout:
+  type: field_layout.third_party_settings
+
+field_layout.third_party_settings:
+  type: mapping
+  label: 'Per-view mode field layout settings'
+  mapping:
+    layout:
+      type: string
+      label: 'Layout'
diff --git a/core/modules/field_layout/field_layout.info.yml b/core/modules/field_layout/field_layout.info.yml
new file mode 100644
index 0000000..62f225a
--- /dev/null
+++ b/core/modules/field_layout/field_layout.info.yml
@@ -0,0 +1,6 @@
+name: 'Field Layout'
+type: module
+description: 'Adds layout capabilities to the Field UI.'
+package: Core (Experimental)
+version: VERSION
+core: 8.x
diff --git a/core/modules/field_layout/field_layout.install b/core/modules/field_layout/field_layout.install
new file mode 100644
index 0000000..865b0e9
--- /dev/null
+++ b/core/modules/field_layout/field_layout.install
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * @file
+ * Contains install and update functions for Field Layout.
+ */
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Entity\Entity\EntityFormDisplay;
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Implements hook_install().
+ */
+function field_layout_install() {
+  // Save each entity display in order to trigger field_layout_entity_presave().
+  $entity_save = function (EntityInterface $entity) {
+    $entity->save();
+  };
+  array_map($entity_save, EntityViewDisplay::loadMultiple());
+  array_map($entity_save, EntityFormDisplay::loadMultiple());
+
+  // Set the layout for known themes.
+  foreach (['bartik', 'seven'] as $theme) {
+    \Drupal::configFactory()->getEditable($theme . '.settings')
+      ->set('third_party_settings.field_layout.layout', $theme)
+      ->save(TRUE);
+  }
+
+  // Refresh the theme info now that the layout has been changed.
+  \Drupal::service('theme_handler')->refreshInfo();
+
+  // Invalidate the render cache since all content will now have a layout.
+  Cache::invalidateTags(['rendered']);
+}
diff --git a/core/modules/field_layout/field_layout.libraries.yml b/core/modules/field_layout/field_layout.libraries.yml
new file mode 100644
index 0000000..d87df5e
--- /dev/null
+++ b/core/modules/field_layout/field_layout.libraries.yml
@@ -0,0 +1,5 @@
+drupal.field_layout.twocol:
+  version: VERSION
+  css:
+    layout:
+      layouts/twocol/twocol.layout.css: {}
diff --git a/core/modules/field_layout/field_layout.module b/core/modules/field_layout/field_layout.module
new file mode 100644
index 0000000..500c53d
--- /dev/null
+++ b/core/modules/field_layout/field_layout.module
@@ -0,0 +1,268 @@
+<?php
+
+/**
+ * @file
+ * Provides hook implementations for Field Layout.
+ */
+
+use Drupal\Core\Entity\ContentEntityFormInterface;
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface;
+use Drupal\field_layout\Entity\FieldLayoutEntityFormDisplay;
+use Drupal\field_layout\Entity\FieldLayoutEntityViewDisplay;
+use Drupal\field_layout\Form\FieldLayoutEntityFormDisplayEditForm;
+use Drupal\field_layout\Form\FieldLayoutEntityViewDisplayEditForm;
+
+/**
+ * Implements hook_help().
+ */
+function field_layout_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    case 'help.page.field_layout':
+      $output = '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('The Field Layout module allows you to arrange fields into regions for content forms and displays.') . '</p>';
+      $output .= '<p>' . t('For more information, see the <a href=":field-layout-documentation">online documentation for the Field Layout module</a>.', [':field-layout-documentation' => 'https://www.drupal.org/documentation/modules/@todo_once_module_name_is_decided_upon']) . '</p>';
+      return $output;
+  }
+}
+
+/**
+ * Implements hook_entity_type_alter().
+ */
+function field_layout_entity_type_alter(array &$entity_types) {
+  /** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */
+  $entity_types['entity_view_display']->setClass(FieldLayoutEntityViewDisplay::class);
+  $entity_types['entity_form_display']->setClass(FieldLayoutEntityFormDisplay::class);
+
+  // The form classes are only needed when Field UI is installed.
+  if (\Drupal::moduleHandler()->moduleExists('field_ui')) {
+    $entity_types['entity_view_display']->setFormClass('edit', FieldLayoutEntityViewDisplayEditForm::class);
+    $entity_types['entity_form_display']->setFormClass('edit', FieldLayoutEntityFormDisplayEditForm::class);
+  }
+}
+
+/**
+ * Implements hook_entity_presave().
+ */
+function field_layout_entity_presave(EntityInterface $entity) {
+  // Whenever creating a new entity display, set the layout to default.
+  if ($entity instanceof EntityDisplayWithLayoutInterface && !$entity->getLayoutId()) {
+    $entity->setLayoutId('default');
+  }
+}
+
+/**
+ * Implements hook_entity_view_alter().
+ */
+function field_layout_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
+  if ($display instanceof EntityDisplayWithLayoutInterface) {
+    _field_layout_apply_layout($build, $display);
+  }
+}
+
+/**
+ * Implements hook_form_alter().
+ */
+function field_layout_form_alter(&$form, FormStateInterface $form_state, $form_id) {
+  $form_object = $form_state->getFormObject();
+  if ($form_object instanceof ContentEntityFormInterface && $display = $form_object->getFormDisplay($form_state)) {
+    if ($display instanceof EntityDisplayWithLayoutInterface) {
+      _field_layout_apply_layout($form, $display, TRUE);
+    }
+  }
+}
+
+/**
+ * Applies the layout to an entity build.
+ *
+ * @param array $build
+ *   A renderable array representing the entity content or form.
+ * @param \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface $display
+ *   The entity display holding the display options configured for the
+ *   entity components.
+ * @param bool $in_form_context
+ *   (optional) If in a form context, an alternate method will be used to render
+ *   fields in their regions. Defaults to FALSE.
+ */
+function _field_layout_apply_layout(array &$build, EntityDisplayWithLayoutInterface $display, $in_form_context = FALSE) {
+  if (!$layout_definition = \Drupal::service('field_layout.layout_repository')->getLayout($display->getLayoutId())) {
+    return;
+  }
+
+  // Wrap the regions in a layout element.
+  $build['field_layout']['#theme'] = $layout_definition['theme'];
+  if (isset($layout_definition['library'])) {
+    $build['field_layout']['#attached']['library'][] = $layout_definition['library'];
+  }
+
+  // Add the regions to the $build in the correct order.
+  foreach ($layout_definition['regions'] as $region => $region_info) {
+    $build['field_layout'][$region] = [];
+    if ($in_form_context) {
+      $build['field_layout'][$region]['#process'][] = '\Drupal\Core\Render\Element\RenderElement::processGroup';
+      $build['field_layout'][$region]['#pre_render'][] = '\Drupal\Core\Render\Element\RenderElement::preRenderGroup';
+    }
+  }
+
+  // Track whether the layout is actually used by any fields.
+  $layout_in_use = FALSE;
+  /** @var \Drupal\Core\Field\FieldDefinitionInterface[] $field_definitions */
+  $field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($display->getTargetEntityTypeId(), $display->getTargetBundle());
+  // Move the field from the top-level of $build into a region-specific section.
+  foreach ($display->getComponents() as $name => $field) {
+    // If the component is a true field, but not configurable, do not alter it.
+    if (isset($field_definitions[$name]) && !$field_definitions[$name]->isDisplayConfigurable($display->get('displayContext'))) {
+      continue;
+    }
+
+    if (isset($build[$name]) && isset($field['region'])) {
+      $layout_in_use = TRUE;
+      // If this is a form, #group can be used to relocate the fields. This
+      // avoids breaking hook_form_alter() implementations by not actually
+      // moving the field in the form structure.
+      if ($in_form_context) {
+        $build[$name]['#group'] = $field['region'];
+      }
+      else {
+        $build['field_layout'][$field['region']][$name] = $build[$name];
+        unset($build[$name]);
+      }
+    }
+  }
+  // Remove any field layout settings if no fields were moved.
+  // @todo Remove this when https://www.drupal.org/node/953034 is resolved.
+  if (!$layout_in_use) {
+    unset($build['field_layout']);
+  }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function field_layout_theme($existing, $type, $theme, $path) {
+  $hooks = [];
+  $layout_definitions = \Drupal::service('field_layout.layout_repository')->getDefinitions();
+  foreach ($layout_definitions as $layout_definition) {
+    $hooks[$layout_definition['theme']] = [
+      'render element' => 'content',
+      'template' => $layout_definition['template'],
+      'path' => $layout_definition['path'],
+    ];
+  }
+  return $hooks;
+}
+
+/**
+ * Implements hook_form_system_theme_settings_alter().
+ */
+function field_layout_form_system_theme_settings_alter(&$form, FormStateInterface $form_state) {
+  $build_info = $form_state->getBuildInfo();
+  if (!isset($build_info['args'][0]) || !($theme = $build_info['args'][0])) {
+    return;
+  }
+
+  $form['field_layouts'] = [
+    '#type' => 'details',
+    '#title' => t('Layout'),
+    '#open' => TRUE,
+  ];
+  $field_layout_repository = \Drupal::service('field_layout.layout_repository');
+  $layout_options = [];
+  foreach ($field_layout_repository->getGroupedDefinitions() as $category => $layouts) {
+    foreach ($layouts as $name => $layout) {
+      $layout_options[$category][$name] = $layout['label'];
+    }
+  }
+  $form['field_layouts']['field_layout'] = [
+    '#type' => 'select',
+    '#title' => t('Select a layout'),
+    '#options' => $layout_options,
+    '#default_value' => \Drupal::config($theme . '.settings')->get('third_party_settings.field_layout.layout') ?: 'default',
+  ];
+  // Color module removes values from $form_state, so run first.
+  array_unshift($form['#submit'], 'field_layout_system_theme_settings_submit');
+}
+
+/**
+ * @todo.
+ */
+function field_layout_system_theme_settings_submit(array &$form, FormStateInterface $form_state) {
+  $theme = $form_state->getValue('theme');
+  $config = \Drupal::configFactory()->getEditable($theme . '.settings');
+  $old_layout = $config->get('third_party_settings.field_layout.layout');
+  $new_layout = $form_state->getValue('field_layout');
+  $config
+    ->set('third_party_settings.field_layout.layout', $new_layout)
+    ->save();
+
+  // Refresh the theme info now that the layout has been changed.
+  \Drupal::service('theme_handler')->refreshInfo();
+
+  if ($new_layout !== $old_layout) {
+    // @todo Devise a mechanism for mapping old regions to new ones in
+    //   https://www.drupal.org/node/2796877.
+    $layout_definition = \Drupal::service('field_layout.layout_repository')->getLayout($new_layout);
+    $new_region = isset($layout_definition['default_region']) ? $layout_definition['default_region'] : key($layout_definition['regions']);
+
+    /** @var \Drupal\block\BlockInterface[] $blocks */
+    $blocks = \Drupal::entityTypeManager()->getStorage('block')->loadByProperties(['theme' => $theme]);
+    foreach ($blocks as $block) {
+      $block->setRegion($new_region)->save();
+    }
+  }
+
+  // Remove the value to prevent theme_settings_convert_to_config() from
+  // directly saving it.
+  $form_state->unsetValue('field_layout');
+}
+
+/**
+ * Implements hook_system_info_alter().
+ */
+function field_layout_system_info_alter(array &$info, Extension $file, $type) {
+  if ($file->getType() === 'theme' && $layout_definition = _field_layout_get_layout_for_theme($file->getName())) {
+    unset($info['regions'], $info['regions_hidden']);
+    foreach ($layout_definition['regions'] as $region => $region_info) {
+      $info['regions'][$region] = $region_info['label']->getUntranslatedString();
+      if (isset($region_info['hidden'])) {
+        $info['regions_hidden'][] = $region;
+      }
+    }
+  }
+}
+
+/**
+ * Implements hook_element_info_alter().
+ */
+function field_layout_element_info_alter(array &$info) {
+  if (isset($info['page'])) {
+    $info['page']['#pre_render'][] = 'field_layout_page_pre_render';
+  }
+}
+
+/**
+ * @todo.
+ */
+function field_layout_page_pre_render($element) {
+  $theme = \Drupal::theme()->getActiveTheme()->getName();
+  if ($layout_definition = _field_layout_get_layout_for_theme($theme)) {
+    $element['#theme'] = $layout_definition['theme'];
+    if (isset($layout_definition['library'])) {
+      $element['#attached']['library'][] = $layout_definition['library'];
+    }
+  }
+  return $element;
+}
+
+/**
+ * @todo.
+ */
+function _field_layout_get_layout_for_theme($theme) {
+  $layout_id = \Drupal::config($theme . '.settings')->get('third_party_settings.field_layout.layout');
+  return \Drupal::service('field_layout.layout_repository')->getLayout($layout_id);
+}
diff --git a/core/modules/field_layout/field_layout.services.yml b/core/modules/field_layout/field_layout.services.yml
new file mode 100644
index 0000000..95c7162
--- /dev/null
+++ b/core/modules/field_layout/field_layout.services.yml
@@ -0,0 +1,4 @@
+services:
+  field_layout.layout_repository:
+    class: Drupal\field_layout\LayoutRepository
+    arguments: ['@module_handler', '@theme_handler', '@string_translation']
diff --git a/core/modules/field_layout/layouts/twocol/field-layout--twocol.html.twig b/core/modules/field_layout/layouts/twocol/field-layout--twocol.html.twig
new file mode 100644
index 0000000..4dffc01
--- /dev/null
+++ b/core/modules/field_layout/layouts/twocol/field-layout--twocol.html.twig
@@ -0,0 +1,28 @@
+{#
+/**
+ * @file
+ * Default theme implementation to display a two column layout.
+ *
+ * Available variables:
+ * - content: The content for this layout.
+ * - attributes: HTML attributes for the layout <div>.
+ *
+ * @ingroup themeable
+ */
+#}
+{%
+set classes = [
+'field-layout--twocol',
+]
+%}
+{% if content %}
+  <div{{ attributes.addClass(classes) }}>
+    <div class="field-layout-region field-layout-region--left">
+      {{ content.left }}
+    </div>
+
+    <div class="field-layout-region field-layout-region--right">
+      {{ content.right }}
+    </div>
+  </div>
+{% endif %}
diff --git a/core/modules/field_layout/layouts/twocol/twocol.layout.css b/core/modules/field_layout/layouts/twocol/twocol.layout.css
new file mode 100644
index 0000000..6803d5b
--- /dev/null
+++ b/core/modules/field_layout/layouts/twocol/twocol.layout.css
@@ -0,0 +1,8 @@
+.field-layout--twocol {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
+}
+.field-layout--twocol > .field-layout-region {
+  flex: 0 1 50%;
+}
diff --git a/core/modules/field_layout/src/Display/EntityDisplayWithLayoutInterface.php b/core/modules/field_layout/src/Display/EntityDisplayWithLayoutInterface.php
new file mode 100644
index 0000000..f98087f
--- /dev/null
+++ b/core/modules/field_layout/src/Display/EntityDisplayWithLayoutInterface.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace Drupal\field_layout\Display;
+
+use Drupal\Core\Entity\Display\EntityDisplayInterface;
+
+/**
+ * Provides a common interface for entity displays that have layout.
+ */
+interface EntityDisplayWithLayoutInterface extends EntityDisplayInterface {
+
+  /**
+   * Gets the default region.
+   *
+   * @return string
+   *   The default region for this display.
+   */
+  public function getDefaultRegion();
+
+  /**
+   * Gets the layout ID for this display.
+   *
+   * @return string
+   *   The layout ID.
+   */
+  public function getLayoutId();
+
+  /**
+   * Sets the layout ID for this display.
+   *
+   * @param string|null $layout_id
+   *   Either a valid layout ID, or NULL to remove the layout setting.
+   *
+   * @return $this
+   */
+  public function setLayoutId($layout_id);
+
+}
diff --git a/core/modules/field_layout/src/Entity/FieldLayoutEntityDisplayTrait.php b/core/modules/field_layout/src/Entity/FieldLayoutEntityDisplayTrait.php
new file mode 100644
index 0000000..567e8b4
--- /dev/null
+++ b/core/modules/field_layout/src/Entity/FieldLayoutEntityDisplayTrait.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Drupal\field_layout\Entity;
+
+/**
+ * Provides shared code for entity displays.
+ */
+trait FieldLayoutEntityDisplayTrait {
+
+  /**
+   * Implements \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface::getLayoutId().
+   */
+  public function getLayoutId() {
+    return $this->getThirdPartySetting('field_layout', 'layout');
+  }
+
+  /**
+   * Implements \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface::setLayoutId().
+   */
+  public function setLayoutId($layout_id) {
+    $this->setThirdPartySetting('field_layout', 'layout', $layout_id);
+    return $this;
+  }
+
+}
diff --git a/core/modules/field_layout/src/Entity/FieldLayoutEntityFormDisplay.php b/core/modules/field_layout/src/Entity/FieldLayoutEntityFormDisplay.php
new file mode 100644
index 0000000..87e271f
--- /dev/null
+++ b/core/modules/field_layout/src/Entity/FieldLayoutEntityFormDisplay.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\field_layout\Entity;
+
+use Drupal\Core\Entity\Entity\EntityFormDisplay;
+use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface;
+
+/**
+ * Provides an entity form display entity that has layout.
+ */
+class FieldLayoutEntityFormDisplay extends EntityFormDisplay implements EntityDisplayWithLayoutInterface {
+
+  use FieldLayoutEntityDisplayTrait;
+
+  /**
+   * {@inheritdoc}
+   *
+   * This cannot be provided by the trait due to https://bugs.php.net/bug.php?id=71414
+   * which is fixed in PHP 7.0.6.
+   */
+  public function getDefaultRegion() {
+    $layout_definition = \Drupal::service('field_layout.layout_repository')->getLayout($this->getLayoutId());
+    return isset($layout_definition['default_region']) ? $layout_definition['default_region'] : key($layout_definition['regions']);
+  }
+
+}
diff --git a/core/modules/field_layout/src/Entity/FieldLayoutEntityViewDisplay.php b/core/modules/field_layout/src/Entity/FieldLayoutEntityViewDisplay.php
new file mode 100644
index 0000000..2ae6088
--- /dev/null
+++ b/core/modules/field_layout/src/Entity/FieldLayoutEntityViewDisplay.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\field_layout\Entity;
+
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
+use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface;
+
+/**
+ * Provides an entity view display entity that has layout.
+ */
+class FieldLayoutEntityViewDisplay extends EntityViewDisplay implements EntityDisplayWithLayoutInterface {
+
+  use FieldLayoutEntityDisplayTrait;
+
+  /**
+   * {@inheritdoc}
+   *
+   * This cannot be provided by the trait due to https://bugs.php.net/bug.php?id=71414
+   * which is fixed in PHP 7.0.6.
+   */
+  public function getDefaultRegion() {
+    $layout_definition = \Drupal::service('field_layout.layout_repository')->getLayout($this->getLayoutId());
+    return isset($layout_definition['default_region']) ? $layout_definition['default_region'] : key($layout_definition['regions']);
+  }
+
+}
diff --git a/core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php b/core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php
new file mode 100644
index 0000000..ac2ae1e
--- /dev/null
+++ b/core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php
@@ -0,0 +1,118 @@
+<?php
+
+namespace Drupal\field_layout\Form;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface;
+
+/**
+ * Provides shared code for entity display forms.
+ */
+trait FieldLayoutEntityDisplayFormTrait {
+
+  /**
+   * The field layout repository.
+   *
+   * @var \Drupal\field_layout\LayoutRepositoryInterface
+   */
+  protected $fieldLayoutRepository;
+
+  /**
+   * Overrides \Drupal\field_ui\Form\EntityDisplayFormBase::getRegions().
+   */
+  public function getRegions() {
+    $regions = [];
+
+    $layout = $this->fieldLayoutRepository->getLayout($this->getEntity()->getLayoutId());
+    foreach ($layout['regions'] as $name => $region) {
+      $regions[$name] = [
+        'title' => $region['label'],
+        'message' => $this->t('No field is displayed.')
+      ];
+    }
+
+    $regions['hidden'] = [
+      'title' => $this->t('Disabled', [], ['context' => 'Plural']),
+      'message' => $this->t('No field is hidden.')
+    ];
+
+    return $regions;
+  }
+
+  /**
+   * Adds the field layout section to the form.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return array
+   */
+  protected function addFieldLayout(array $form, FormStateInterface $form_state) {
+    $form['#entity_builders'][] = [static::class, 'updateFieldLayout'];
+
+    $form['field_layouts'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Layout for @bundle in @view_mode', [
+        '@bundle' => str_replace('_', ' ', $this->getEntity()->getTargetBundle()),
+        '@view_mode' => str_replace('_', ' ', $this->getEntity()->getMode()),
+      ]),
+    ];
+    $layout_options = [];
+    foreach ($this->fieldLayoutRepository->getGroupedDefinitions() as $category => $layouts) {
+      foreach ($layouts as $name => $layout) {
+        $layout_options[$category][$name] = $layout['label'];
+      }
+    }
+    $form['field_layouts']['field_layout'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Select a layout'),
+      '#options' => $layout_options,
+      '#default_value' => $this->getEntity()->getLayoutId() ?: 'default',
+    ];
+
+    return $form;
+  }
+
+  /**
+   * An #entity_builder callback to update the Field Layout settings.
+   *
+   * @param string $entity_type_id
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   * @param array $form
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   */
+  public static function updateFieldLayout($entity_type_id, EntityInterface $entity, &$form, FormStateInterface $form_state) {
+    if ($form_state->isSubmitted() && $entity instanceof EntityDisplayWithLayoutInterface) {
+      $old_layout = $entity->getLayoutId();
+      $new_layout = $form_state->getValue('field_layout');
+      $entity->setLayoutId($new_layout);
+      // If the layout is changing, reset all fields.
+      if ($new_layout !== $old_layout) {
+        // @todo Devise a mechanism for mapping old regions to new ones in
+        //   https://www.drupal.org/node/2796877.
+        $new_region = $entity->getDefaultRegion();
+        foreach ($form_state->getValue('fields') as $field_name => $values) {
+          if (($component = $entity->getComponent($field_name)) && $new_region !== 'hidden') {
+            $component['region'] = $new_region;
+            $entity->setComponent($field_name, $component);
+          }
+          else {
+            $entity->removeComponent($field_name);
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Gets the form entity.
+   *
+   * @return \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface
+   *   The current form entity.
+   */
+  abstract public function getEntity();
+
+}
diff --git a/core/modules/field_layout/src/Form/FieldLayoutEntityFormDisplayEditForm.php b/core/modules/field_layout/src/Form/FieldLayoutEntityFormDisplayEditForm.php
new file mode 100644
index 0000000..d57912e
--- /dev/null
+++ b/core/modules/field_layout/src/Form/FieldLayoutEntityFormDisplayEditForm.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\field_layout\Form;
+
+use Drupal\Component\Plugin\PluginManagerBase;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldTypePluginManagerInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\field_layout\LayoutRepositoryInterface;
+use Drupal\field_ui\Form\EntityFormDisplayEditForm;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Edit form for the EntityFormDisplay entity type.
+ */
+class FieldLayoutEntityFormDisplayEditForm extends EntityFormDisplayEditForm {
+
+  use FieldLayoutEntityDisplayFormTrait;
+
+  /**
+   * FieldLayoutEntityFormDisplayEditForm constructor.
+   *
+   * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
+   *   The field type manager.
+   * @param \Drupal\Component\Plugin\PluginManagerBase $plugin_manager
+   *   The widget plugin manager.
+   * @param \Drupal\field_layout\LayoutRepositoryInterface $field_layout_repository
+   *   The field layout repository.
+   */
+  public function __construct(FieldTypePluginManagerInterface $field_type_manager, PluginManagerBase $plugin_manager, LayoutRepositoryInterface $field_layout_repository) {
+    parent::__construct($field_type_manager, $plugin_manager);
+    $this->fieldLayoutRepository = $field_layout_repository;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.field.field_type'),
+      $container->get('plugin.manager.field.widget'),
+      $container->get('field_layout.layout_repository')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+    return $this->addFieldLayout($form, $form_state);
+  }
+
+}
diff --git a/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php b/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php
new file mode 100644
index 0000000..5c9f26f
--- /dev/null
+++ b/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\field_layout\Form;
+
+use Drupal\Component\Plugin\PluginManagerBase;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldTypePluginManagerInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\field_layout\LayoutRepositoryInterface;
+use Drupal\field_ui\Form\EntityViewDisplayEditForm;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Edit form for the EntityViewDisplay entity type.
+ */
+class FieldLayoutEntityViewDisplayEditForm extends EntityViewDisplayEditForm {
+
+  use FieldLayoutEntityDisplayFormTrait;
+
+  /**
+   * FieldLayoutEntityViewDisplayEditForm constructor.
+   *
+   * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
+   *   The field type manager.
+   * @param \Drupal\Component\Plugin\PluginManagerBase $plugin_manager
+   *   The formatter plugin manager.
+   * @param \Drupal\field_layout\LayoutRepositoryInterface $field_layout_repository
+   *   The field layout repository.
+   */
+  public function __construct(FieldTypePluginManagerInterface $field_type_manager, PluginManagerBase $plugin_manager, LayoutRepositoryInterface $field_layout_repository) {
+    parent::__construct($field_type_manager, $plugin_manager);
+    $this->fieldLayoutRepository = $field_layout_repository;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.field.field_type'),
+      $container->get('plugin.manager.field.formatter'),
+      $container->get('field_layout.layout_repository')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+    return $this->addFieldLayout($form, $form_state);
+  }
+
+}
diff --git a/core/modules/field_layout/src/LayoutRepository.php b/core/modules/field_layout/src/LayoutRepository.php
new file mode 100644
index 0000000..81347ec
--- /dev/null
+++ b/core/modules/field_layout/src/LayoutRepository.php
@@ -0,0 +1,252 @@
+<?php
+
+namespace Drupal\field_layout;
+
+use Drupal\Component\Plugin\Discovery\DiscoveryTrait;
+use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Extension\ThemeHandlerInterface;
+use Drupal\Core\Plugin\CategorizingPluginManagerTrait;
+use Drupal\Core\StringTranslation\TranslationInterface;
+
+/**
+ * Provides a repository for field layouts.
+ */
+class LayoutRepository implements LayoutRepositoryInterface {
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * The theme handler.
+   *
+   * @var \Drupal\Core\Extension\ThemeHandlerInterface
+   */
+  protected $themeHandler;
+
+  use DiscoveryTrait;
+  use CategorizingPluginManagerTrait {
+    getModuleHandler as protected;
+    getCategories as protected;
+    getSortedDefinitions as protected;
+  }
+
+  /**
+   * LayoutRepository constructor.
+   *
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
+   *   The theme handler.
+   * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
+   *   The string translation service.
+   */
+  public function __construct(ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler, TranslationInterface $string_translation) {
+    $this->moduleHandler = $module_handler;
+    $this->themeHandler = $theme_handler;
+    $this->stringTranslation = $string_translation;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefinitions() {
+    // @todo Replace with layout_plugin in https://www.drupal.org/node/2296423.
+    $layouts = [
+      'default' => [
+        'label' => $this->t('Default'),
+        'theme' => 'field_layout__default',
+        'provider' => 'field_layout',
+        'category' => $this->t('Columns: 1'),
+        'default_region' => 'content',
+        'regions' => [
+          'content' => [
+            'label' => $this->t('Content'),
+          ],
+        ],
+      ],
+      'twocol' => [
+        'label' => $this->t('Two column'),
+        'library' => 'field_layout/drupal.field_layout.twocol',
+        'theme' => 'field_layout__twocol',
+        'path' => 'layouts/twocol',
+        'provider' => 'field_layout',
+        'category' => $this->t('Columns: 2'),
+        'default_region' => 'left',
+        'regions' => [
+          'left' => [
+            'label' => $this->t('Left'),
+          ],
+          'right' => [
+            'label' => $this->t('Right'),
+          ],
+        ],
+      ],
+      'bartik' => [
+        'label' => $this->t('Bartik'),
+        'library' => 'bartik/global-styling',
+        'theme' => 'page',
+        'provider' => 'bartik',
+        'category' => $this->t('Theme-specific'),
+        'default_region' => 'content',
+        'regions' => [
+          'header' => [
+            'label' => $this->t('Header'),
+          ],
+          'primary_menu' => [
+            'label' => $this->t('Primary menu'),
+          ],
+          'secondary_menu' => [
+            'label' => $this->t('Secondary menu'),
+          ],
+          'page_top' => [
+            'label' => $this->t('Page top'),
+            'hidden' => TRUE,
+          ],
+          'page_bottom' => [
+            'label' => $this->t('Page bottom'),
+            'hidden' => TRUE,
+          ],
+          'highlighted' => [
+            'label' => $this->t('Highlighted'),
+          ],
+          'featured_top' => [
+            'label' => $this->t('Featured top'),
+          ],
+          'breadcrumb' => [
+            'label' => $this->t('Breadcrumb'),
+          ],
+          'content' => [
+            'label' => $this->t('Content'),
+          ],
+          'sidebar_first' => [
+            'label' => $this->t('Sidebar first'),
+          ],
+          'sidebar_second' => [
+            'label' => $this->t('Sidebar second'),
+          ],
+          'featured_bottom_first' => [
+            'label' => $this->t('Featured bottom first'),
+          ],
+          'featured_bottom_second' => [
+            'label' => $this->t('Featured bottom second'),
+          ],
+          'featured_bottom_third' => [
+            'label' => $this->t('Featured bottom third'),
+          ],
+          'footer_first' => [
+            'label' => $this->t('Footer first'),
+          ],
+          'footer_second' => [
+            'label' => $this->t('Footer second'),
+          ],
+          'footer_third' => [
+            'label' => $this->t('Footer third'),
+          ],
+          'footer_fourth' => [
+            'label' => $this->t('Footer fourth'),
+          ],
+          'footer_fifth' => [
+            'label' => $this->t('Footer fifth'),
+          ],
+        ],
+      ],
+      'seven' => [
+        'label' => $this->t('Seven'),
+        'library' => 'seven/global-styling',
+        'theme' => 'page',
+        'provider' => 'seven',
+        'category' => $this->t('Theme-specific'),
+        'default_region' => 'content',
+        'regions' => [
+          'header' => [
+            'label' => $this->t('Header'),
+          ],
+          'pre_content' => [
+            'label' => $this->t('Pre-content'),
+          ],
+          'breadcrumb' => [
+            'label' => $this->t('Breadcrumb'),
+          ],
+          'highlighted' => [
+            'label' => $this->t('Highlighted'),
+          ],
+          'help' => [
+            'label' => $this->t('Help'),
+          ],
+          'content' => [
+            'label' => $this->t('Content'),
+          ],
+          'page_top' => [
+            'label' => $this->t('Page top'),
+            'hidden' => TRUE,
+          ],
+          'page_bottom' => [
+            'label' => $this->t('Page bottom'),
+            'hidden' => TRUE,
+          ],
+          'sidebar_first' => [
+            'label' => $this->t('Sidebar first'),
+            'hidden' => TRUE,
+          ],
+        ],
+      ],
+    ];
+    foreach ($layouts as $layout_id => &$layout) {
+      $this->processDefinition($layout, $layout_id);
+    }
+    return $layouts;
+  }
+
+  /**
+   * Performs extra processing on layout definitions.
+   */
+  protected function processDefinition(&$definition, $plugin_id) {
+    $definition['layout'] = $plugin_id;
+    // @todo Remove when adding layout_plugin in https://www.drupal.org/node/2296423.
+
+    // Add the module or theme path to the 'path'.
+    if ($this->moduleHandler->moduleExists($definition['provider'])) {
+      $definition['provider_type'] = 'module';
+      $base_path = $this->moduleHandler->getModule($definition['provider'])->getPath();
+    }
+    elseif ($this->themeHandler->themeExists($definition['provider'])) {
+      $definition['provider_type'] = 'theme';
+      $base_path = $this->themeHandler->getTheme($definition['provider'])->getPath();
+    }
+    else {
+      $base_path = '';
+    }
+    $definition['path'] = !empty($definition['path']) ? $base_path . '/' . $definition['path'] : NULL;
+
+    // Either a theme or a template must be defined.
+    if (empty($definition['template']) && empty($definition['theme'])) {
+      throw new InvalidPluginDefinitionException($plugin_id);
+    }
+
+    if (empty($definition['template'])) {
+      $definition['template'] = strtr($definition['theme'], '_', '-');
+    }
+    elseif (empty($definition['theme'])) {
+      $definition['theme'] = strtr($definition['template'], '-', '_');
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLayout($layout_id) {
+    $layout_id = $layout_id ?: 'default';
+    $layout_definitions = $this->getDefinitions();
+    if (!$layout_id || !isset($layout_definitions[$layout_id])) {
+      return [];
+    }
+
+    return $layout_definitions[$layout_id];
+  }
+
+}
diff --git a/core/modules/field_layout/src/LayoutRepositoryInterface.php b/core/modules/field_layout/src/LayoutRepositoryInterface.php
new file mode 100644
index 0000000..c6862f8
--- /dev/null
+++ b/core/modules/field_layout/src/LayoutRepositoryInterface.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Drupal\field_layout;
+
+use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
+
+/**
+ * Provides the interface for a repository of field layouts.
+ */
+interface LayoutRepositoryInterface extends DiscoveryInterface {
+
+  /**
+   * Gets sorted plugin definitions grouped by category.
+   *
+   * In addition to grouping, both categories and its entries are sorted,
+   * whereas plugin definitions are sorted by label.
+   *
+   * @param array[]|null $definitions
+   *   (optional) The plugin definitions to group. If omitted, all plugin
+   *   definitions are used.
+   *
+   * @return array[]
+   *   Keys are category names, and values are arrays of which the keys are
+   *   plugin IDs and the values are plugin definitions.
+   */
+  public function getGroupedDefinitions(array $definitions = NULL);
+
+  /**
+   * Gets the layout definition for a given layout ID.
+   *
+   * @param string|null $layout_id
+   *   The layout ID. If NULL, a default will be provided.
+   *
+   * @return mixed[]
+   *   The layout definition for the given display.
+   */
+  public function getLayout($layout_id);
+
+}
diff --git a/core/modules/field_layout/templates/field-layout--default.html.twig b/core/modules/field_layout/templates/field-layout--default.html.twig
new file mode 100644
index 0000000..3f8bb17
--- /dev/null
+++ b/core/modules/field_layout/templates/field-layout--default.html.twig
@@ -0,0 +1,24 @@
+{#
+/**
+ * @file
+ * Default theme implementation to display a default layout.
+ *
+ * Available variables:
+ * - content: The content for this layout.
+ * - attributes: HTML attributes for the layout <div>.
+ *
+ * @ingroup themeable
+ */
+#}
+{%
+set classes = [
+'field-layout--default',
+]
+%}
+{% if content %}
+<div{{ attributes.addClass(classes) }}>
+  <div class="field-layout-region field-layout-region--content">
+    {{ content }}
+  </div>
+</div>
+{% endif %}
diff --git a/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.info.yml b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.info.yml
new file mode 100644
index 0000000..4d699e4
--- /dev/null
+++ b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.info.yml
@@ -0,0 +1,8 @@
+name: 'Field Layout test'
+type: module
+description: 'Support module for Field Layout tests.'
+core: 8.x
+package: Testing
+version: VERSION
+dependencies:
+  - entity_test
diff --git a/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.routing.yml b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.routing.yml
new file mode 100644
index 0000000..bcea288
--- /dev/null
+++ b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.routing.yml
@@ -0,0 +1,7 @@
+entity.entity_test.test_view_mode:
+  path: '/entity_test/{entity_test}/test'
+  defaults:
+    _entity_view: 'entity_test.test'
+    _title: 'Test test view mode'
+  requirements:
+    _entity_access: 'entity_test.view'
diff --git a/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php b/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php
new file mode 100644
index 0000000..04d0810
--- /dev/null
+++ b/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php
@@ -0,0 +1,253 @@
+<?php
+
+namespace Drupal\Tests\field_layout\FunctionalJavascript;
+
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+
+/**
+ * Tests using field layout for entity displays.
+ *
+ * @group field_layout
+ */
+class FieldLayoutTest extends JavascriptTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['field_layout', 'field_ui', 'node', 'field_layout_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->createContentType([
+      'type' => 'article',
+    ]);
+    $this->createNode([
+      'type' => 'article',
+      'title' => 'The node title',
+      'body' => [[
+        'value' => 'The node body',
+      ]],
+    ]);
+    $entity = EntityTest::create([
+      'name' => 'The name for this entity',
+      'field_test_text' => [[
+        'value' => 'The field test text value',
+      ]],
+    ]);
+    $entity->save();
+    $this->drupalLogin($this->drupalCreateUser([
+      'access administration pages',
+      'administer content types',
+      'administer nodes',
+      'administer node fields',
+      'administer node display',
+      'administer node form display',
+      'view test entity',
+      'administer entity_test content',
+      'administer entity_test fields',
+      'administer entity_test display',
+      'administer entity_test form display',
+      'view the administration theme',
+    ]));
+  }
+
+  /**
+   * Tests that layouts are unique per-view mode.
+   */
+  public function testEntityViewModes() {
+    // By default, the field is not visible.
+    $this->drupalGet('entity_test/1/test');
+    $this->assertSession()->elementNotExists('css', '.field-layout-region--content .field--name-field-test-text');
+    $this->drupalGet('entity_test/1');
+    $this->assertSession()->elementNotExists('css', '.field-layout-region--content .field--name-field-test-text');
+
+    // Change the layout for the "test" view mode. See
+    // core.entity_view_mode.entity_test.test.yml.
+    $this->drupalGet('entity_test/structure/entity_test/display');
+    $this->click('#edit-modes');
+    $this->getSession()->getPage()->checkField('display_modes_custom[test]');
+    $this->submitForm([], 'Save');
+    $this->clickLink('configure them');
+    $this->getSession()->getPage()->pressButton('Show row weights');
+    $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'content');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->submitForm([], 'Save');
+
+    // Each view mode has a different layout.
+    $this->drupalGet('entity_test/1/test');
+    $this->assertSession()->elementExists('css', '.field-layout-region--content .field--name-field-test-text');
+    $this->drupalGet('entity_test/1');
+    $this->assertSession()->elementNotExists('css', '.field-layout-region--content .field--name-field-test-text');
+  }
+
+  /**
+   * Tests the use of field layout for entity form displays.
+   */
+  public function testEntityForm() {
+    // By default, the default layout is used.
+    $this->drupalGet('entity_test/manage/1/edit');
+    $this->assertFieldInRegion('field_test_text[0][value]', 'content');
+
+    // The default layout is in use.
+    $this->drupalGet('entity_test/structure/entity_test/form-display');
+    $this->assertEquals(['Content', 'Disabled'], $this->getRegionTitles());
+
+    // Switch the layout to two columns.
+    $this->click('#edit-field-layouts');
+    $this->getSession()->getPage()->selectFieldOption('field_layout', 'twocol');
+    $this->submitForm([], 'Save');
+
+    // The field is moved to the default region for the new layout.
+    $this->assertSession()->pageTextContains('Your settings have been saved.');
+    $this->assertEquals(['Left', 'Right', 'Disabled'], $this->getRegionTitles());
+
+    $this->drupalGet('entity_test/manage/1/edit');
+    // No fields are visible, and the regions don't display when empty.
+    $this->assertFieldInRegion('field_test_text[0][value]', 'left');
+    $this->assertSession()->elementExists('css', '.field-layout-region--left .field--name-field-test-text');
+
+    // After a refresh the new regions are still there.
+    $this->drupalGet('entity_test/structure/entity_test/form-display');
+    $this->assertEquals(['Left', 'Right', 'Disabled'], $this->getRegionTitles());
+
+    // Drag the field to the right region.
+    $field_test_text_row = $this->getSession()->getPage()->find('css', '#field-test-text');
+    $left_region_row = $this->getSession()->getPage()->find('css', '.region-right-message');
+    $field_test_text_row->find('css', '.handle')->dragTo($left_region_row);
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->submitForm([], 'Save');
+    $this->assertSession()->pageTextContains('Your settings have been saved.');
+
+    // The new layout is used.
+    $this->drupalGet('entity_test/manage/1/edit');
+    $this->assertSession()->elementExists('css', '.field-layout-region--right .field--name-field-test-text');
+    $this->assertFieldInRegion('field_test_text[0][value]', 'right');
+
+    // Move the field to the right region without tabledrag.
+    $this->drupalGet('entity_test/structure/entity_test/form-display');
+    $this->getSession()->getPage()->pressButton('Show row weights');
+    $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'right');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->submitForm([], 'Save');
+    $this->assertSession()->pageTextContains('Your settings have been saved.');
+
+    // The updated region is used.
+    $this->drupalGet('entity_test/manage/1/edit');
+    $this->assertFieldInRegion('field_test_text[0][value]', 'right');
+
+    // The layout is still in use without Field UI.
+    $this->container->get('module_installer')->uninstall(['field_ui']);
+    $this->drupalGet('entity_test/manage/1/edit');
+    $this->assertFieldInRegion('field_test_text[0][value]', 'right');
+  }
+
+  /**
+   * Tests the use of field layout for entity view displays.
+   */
+  public function testEntityView() {
+    // The default layout is in use.
+    $this->drupalGet('entity_test/structure/entity_test/display');
+    $this->assertEquals(['Content', 'Disabled'], $this->getRegionTitles());
+
+    // Switch the layout to two columns.
+    $this->click('#edit-field-layouts');
+    $this->getSession()->getPage()->selectFieldOption('field_layout', 'twocol');
+    $this->submitForm([], 'Save');
+
+    $this->assertSession()->pageTextContains('Your settings have been saved.');
+    $this->assertEquals(['Left', 'Right', 'Disabled'], $this->getRegionTitles());
+
+    $this->drupalGet('entity_test/1');
+    // No fields are visible, and the regions don't display when empty.
+    $this->assertSession()->elementNotExists('css', '.field-layout--twocol');
+    $this->assertSession()->elementNotExists('css', '.field-layout-region');
+    $this->assertSession()->elementNotExists('css', '.field--name-field-test-text');
+
+    // After a refresh the new regions are still there.
+    $this->drupalGet('entity_test/structure/entity_test/display');
+    $this->assertEquals(['Left', 'Right', 'Disabled'], $this->getRegionTitles());
+
+    // Drag the field to the left region.
+    $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->isSelected());
+    $field_test_text_row = $this->getSession()->getPage()->find('css', '#field-test-text');
+    $left_region_row = $this->getSession()->getPage()->find('css', '.region-left-message');
+    $field_test_text_row->find('css', '.handle')->dragTo($left_region_row);
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertFalse($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->isSelected());
+    $this->submitForm([], 'Save');
+    $this->assertSession()->pageTextContains('Your settings have been saved.');
+
+    // The new layout is used.
+    $this->drupalGet('entity_test/1');
+    $this->assertSession()->elementExists('css', '.field-layout--twocol');
+    $this->assertSession()->elementExists('css', '.field-layout-region--left .field--name-field-test-text');
+
+    // Move the field to the right region without tabledrag.
+    $this->drupalGet('entity_test/structure/entity_test/display');
+    $this->getSession()->getPage()->pressButton('Show row weights');
+    $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'right');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->submitForm([], 'Save');
+    $this->assertSession()->pageTextContains('Your settings have been saved.');
+
+    // The updated region is used.
+    $this->drupalGet('entity_test/1');
+    $this->assertSession()->elementExists('css', '.field-layout-region--right .field--name-field-test-text');
+
+    // The layout is still in use without Field UI.
+    $this->container->get('module_installer')->uninstall(['field_ui']);
+    $this->drupalGet('entity_test/1');
+    $this->assertSession()->elementExists('css', '.field-layout--twocol');
+    $this->assertSession()->elementExists('css', '.field-layout-region--right .field--name-field-test-text');
+  }
+
+  /**
+   * Tests an entity type that has fields shown by default.
+   */
+  public function testNodeView() {
+    // By default, the default layout is used.
+    $this->drupalGet('node/1');
+    $this->assertSession()->elementExists('css', '.field-layout--default');
+    $this->assertSession()->elementExists('css', '.field-layout-region--content .field--name-body');
+
+    $this->drupalGet('admin/structure/types/manage/article/display');
+    $this->assertEquals(['Content', 'Disabled'], $this->getRegionTitles());
+    $this->assertSession()->optionExists('fields[body][region]', 'content');
+  }
+
+  /**
+   * Gets the region titles on the page.
+   *
+   * @return string[]
+   *   An array of region titles.
+   */
+  protected function getRegionTitles() {
+    $region_titles = [];
+    $region_title_elements = $this->getSession()->getPage()->findAll('css', '.region-title td');
+    /** @var \Behat\Mink\Element\NodeElement[] $region_title_elements */
+    foreach ($region_title_elements as $region_title_element) {
+      $region_titles[] = $region_title_element->getText();
+    }
+    return $region_titles;
+  }
+
+  /**
+   * Asserts that a field exists in a given region.
+   *
+   * @param string $field_selector
+   *   The field selector, one of field id|name|label|value.
+   * @param string $region_name
+   *   The machine name of the region.
+   */
+  protected function assertFieldInRegion($field_selector, $region_name) {
+    $region_element = $this->getSession()->getPage()->find('css', ".field-layout-region--$region_name");
+    $this->assertNotNull($region_element);
+    $this->assertSession()->fieldExists($field_selector, $region_element);
+  }
+
+}
diff --git a/core/modules/field_layout/tests/src/Unit/LayoutRepositoryTest.php b/core/modules/field_layout/tests/src/Unit/LayoutRepositoryTest.php
new file mode 100644
index 0000000..9b7ce5a
--- /dev/null
+++ b/core/modules/field_layout/tests/src/Unit/LayoutRepositoryTest.php
@@ -0,0 +1,110 @@
+<?php
+
+namespace Drupal\Tests\field_layout\Unit;
+
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Extension\ThemeHandlerInterface;
+use Drupal\field_layout\LayoutRepository;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\field_layout\LayoutRepository
+ * @group field_layout
+ */
+class LayoutRepositoryTest extends UnitTestCase {
+
+  /**
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * @var \Drupal\Core\Extension\ThemeHandlerInterface
+   */
+  protected $themeHandler;
+
+  /**
+   * @var \Drupal\field_layout\LayoutRepository
+   */
+  protected $layoutRepository;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->moduleHandler = $this->prophesize(ModuleHandlerInterface::class);
+    $this->moduleHandler->moduleExists('field_layout')->willReturn(TRUE);
+    $extension = new Extension('/', 'module', 'core/modules/field_layout/field_layout.info.yml');
+    $this->moduleHandler->getModule('field_layout')->willReturn($extension);
+
+    $this->themeHandler = $this->prophesize(ThemeHandlerInterface::class);
+
+    $this->layoutRepository = new LayoutRepository($this->moduleHandler->reveal(), $this->themeHandler->reveal(), $this->getStringTranslationStub());
+  }
+
+  /**
+   * @covers ::getDefinitions
+   */
+  public function testGetDefinitions() {
+    $expected = ['default', 'twocol'];
+
+    $layout_definitions = $this->layoutRepository->getDefinitions();
+    $this->assertEquals($expected, array_keys($layout_definitions));
+  }
+
+  /**
+   * @covers ::getLayout
+   */
+  public function testGetLayout() {
+    $expected = [
+      'label' => 'Two column',
+      'theme' => 'field_layout__twocol',
+      'provider' => 'field_layout',
+      'category' => 'Columns: 2',
+      'default_region' => 'left',
+      'regions' => [
+        'left' => [
+          'label' => 'Left',
+        ],
+        'right' => [
+          'label' => 'Right',
+        ],
+      ],
+      'path' => 'core/modules/field_layout/layouts/twocol',
+      'template' => 'field-layout--twocol',
+      'layout' => 'twocol',
+      'library' => 'field_layout/drupal.field_layout.twocol',
+      'provider_type' => 'module',
+    ];
+    $layout_definition = $this->layoutRepository->getLayout('twocol');
+    $this->assertEquals($expected, $layout_definition);
+  }
+
+  /**
+   * @covers ::getLayout
+   */
+  public function testGetLayoutEmpty() {
+    $expected = [
+      'label' => 'Default',
+      'theme' => 'field_layout__default',
+      'provider' => 'field_layout',
+      'category' => 'Columns: 1',
+      'default_region' => 'content',
+      'regions' => [
+        'content' => [
+          'label' => 'Content',
+        ],
+      ],
+      'path' => NULL,
+      'template' => 'field-layout--default',
+      'layout' => 'default',
+      'provider_type' => 'module',
+    ];
+    $layout_definition = $this->layoutRepository->getLayout(NULL);
+    $this->assertEquals($expected, $layout_definition);
+  }
+
+}
diff --git a/core/modules/field_ui/field_ui.js b/core/modules/field_ui/field_ui.js
index c9ff216..e11a2e8 100644
--- a/core/modules/field_ui/field_ui.js
+++ b/core/modules/field_ui/field_ui.js
@@ -297,7 +297,7 @@ else if ($this.is('.region-empty')) {
     /**
      * Returns the region corresponding to the current form values of the row.
      *
-     * @return {string}
+     * @returns {string}
      *   Either 'hidden' or 'content'.
      */
     getType: function () {
diff --git a/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php b/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php
index cbf86d2..b149769 100644
--- a/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php
+++ b/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php
@@ -137,7 +137,7 @@ public function testFormat($string, array $args, $expected, $message, $expected_
     UrlHelper::setAllowedProtocols(['http', 'https', 'mailto']);
 
     $result = SafeMarkup::format($string, $args);
-    $this->assertEquals($expected, $result, $message);
+    $this->assertEquals($expected, (string) $result, $message);
     $this->assertEquals($expected_is_safe, $result instanceof MarkupInterface, 'SafeMarkup::format correctly sets the result as safe or not safe.');
 
     foreach ($args as $arg) {
@@ -171,6 +171,8 @@ function providerFormat() {
     $tests['non-url-with-colon'] = ['Hey giraffe <a href=":url">MUUUH</a>', [':url' => "llamas: they are not URLs"], 'Hey giraffe <a href=" they are not URLs">MUUUH</a>', '', TRUE];
     $tests['non-url-with-html'] = ['Hey giraffe <a href=":url">MUUUH</a>', [':url' => "<span>not a url</span>"], 'Hey giraffe <a href="&lt;span&gt;not a url&lt;/span&gt;">MUUUH</a>', '', TRUE];
 
+    // Tests non-standard placeholders.
+    $tests['non-standard-placeholder'] = ['Hey risky', ['risky' => "<script>alert('foo');</script>"], 'Hey <em class="placeholder">&lt;script&gt;alert(&#039;foo&#039;);&lt;/script&gt;</em>', '', TRUE];
     return $tests;
   }
   /**
diff --git a/core/themes/bartik/bartik.info.yml b/core/themes/bartik/bartik.info.yml
index 130d583..13df6e9 100644
--- a/core/themes/bartik/bartik.info.yml
+++ b/core/themes/bartik/bartik.info.yml
@@ -25,23 +25,3 @@ ckeditor_stylesheets:
   - css/components/captions.css
   - css/components/table.css
   - css/components/text-formatted.css
-regions:
-  header: Header
-  primary_menu: 'Primary menu'
-  secondary_menu: 'Secondary menu'
-  page_top: 'Page top'
-  page_bottom: 'Page bottom'
-  highlighted: Highlighted
-  featured_top: 'Featured top'
-  breadcrumb: Breadcrumb
-  content: Content
-  sidebar_first: 'Sidebar first'
-  sidebar_second: 'Sidebar second'
-  featured_bottom_first: 'Featured bottom first'
-  featured_bottom_second: 'Featured bottom second'
-  featured_bottom_third: 'Featured bottom third'
-  footer_first: 'Footer first'
-  footer_second: 'Footer second'
-  footer_third: 'Footer third'
-  footer_fourth: 'Footer fourth'
-  footer_fifth: 'Footer fifth'
diff --git a/core/themes/seven/seven.info.yml b/core/themes/seven/seven.info.yml
index 369515e..2ab7a6d 100644
--- a/core/themes/seven/seven.info.yml
+++ b/core/themes/seven/seven.info.yml
@@ -47,15 +47,3 @@ libraries-extend:
     - seven/tour-styling
 quickedit_stylesheets:
   - css/components/quickedit.css
-regions:
-  header: 'Header'
-  pre_content: 'Pre-content'
-  breadcrumb: Breadcrumb
-  highlighted: Highlighted
-  help: Help
-  content: Content
-  page_top: 'Page top'
-  page_bottom: 'Page bottom'
-  sidebar_first: 'First sidebar'
-regions_hidden:
-  - sidebar_first
