diff --git a/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonCookieTranslationsTest.php b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonCookieTranslationsTest.php
new file mode 100644
index 0000000..d67c2cd
--- /dev/null
+++ b/core/modules/hal/tests/src/Functional/EntityResource/Node/NodeHalJsonCookieTranslationsTest.php
@@ -0,0 +1,198 @@
+<?php
+
+namespace Drupal\Tests\hal\Functional\EntityResource\Node;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Cache\Cache;
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\language\Entity\ContentLanguageSettings;
+use Drupal\Tests\hal\Functional\EntityResource\HalEntityNormalizationTrait;
+use Drupal\user\Entity\User;
+use GuzzleHttp\RequestOptions;
+
+/**
+ * @group hal
+ */
+class NodeHalJsonCookieTranslationsTest extends NodeHalJsonCookieTest {
+
+  use HalEntityNormalizationTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['hal', 'content_translation'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $format = 'hal_json';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $mimeType = 'application/hal+json';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $patchProtectedFieldNames = [
+    'revision_timestamp',
+    'revision_uid',
+    'changed',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedCacheContexts() {
+    return Cache::mergeContexts(
+      parent::getExpectedCacheContexts(),
+      ['languages:language_content', 'languages:language_interface']
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUpAuthorization($method) {
+    parent::setUpAuthorization($method);
+    $this->grantPermissionsToTestedRole(['translate any entity', 'administer nodes']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function createEntity() {
+    if (!ConfigurableLanguage::load('de')) {
+      ConfigurableLanguage::createFromLangcode('de')->save();
+    }
+
+    /** @var \Drupal\node\NodeInterface $node */
+    $node = parent::createEntity();
+
+    ContentLanguageSettings::loadByEntityTypeBundle('node', 'camelids')
+      ->setLanguageAlterable(TRUE)
+      ->setDefaultLangcode('en')
+      ->save();
+
+    $translation = $node->addTranslation('de', $node->toArray());
+    $translation->get('title')->value = 'Lama';
+    $node->save();
+    return $node;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedNormalizedEntity() {
+    $normalization = parent::getExpectedNormalizedEntity();
+
+    foreach ($this->entity->getFieldDefinitions() as $field_name => $field_definition) {
+      // @todo It seems that currently two items exist if the *storage* is
+      //   translatable, should be when the field is?
+      if ($field_definition->getFieldStorageDefinition()->isTranslatable() && isset($normalization[$field_name])) {
+        $normalization[$field_name][1] = [
+          'lang' => 'de',
+        ] + $normalization[$field_name][0];
+      }
+    }
+
+    $normalization['title'][1]['value'] = 'Lama';
+    $normalization['langcode'][1]['value'] = 'de';
+    $normalization['default_langcode'][1]['value'] = '0';
+
+    $author = User::load($this->entity->getOwnerId());
+    $normalization['_links'][$this->baseUrl . '/rest/relation/node/camelids/uid'][1] = [
+      'href' => $this->baseUrl . '/user/' . $author->id() . '?_format=hal_json',
+      'lang' => 'de',
+    ];
+    $normalization['_embedded'][$this->baseUrl . '/rest/relation/node/camelids/uid'][1] = [
+      '_links' => [
+        'self' => [
+          'href' => $this->baseUrl . '/user/' . $author->id() . '?_format=hal_json',
+        ],
+        'type' => [
+          'href' => $this->baseUrl . '/rest/type/user/user',
+        ],
+      ],
+      'uuid' => [
+        ['value' => $author->uuid()]
+      ],
+      'lang' => 'de',
+    ];
+
+    return $normalization;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function testPost() {
+    parent::testPost();
+
+    // Create an entity with a translation.
+    $normalized_entity = $this->getExpectedNormalizedEntity();
+    unset($normalized_entity['nid']);
+    unset($normalized_entity['vid']);
+    unset($normalized_entity['uuid']);
+    unset($normalized_entity['changed']);
+    unset($normalized_entity['revision_timestamp']);
+    unset($normalized_entity['_embedded'][$this->baseUrl . '/rest/relation/node/camelids/revision_uid']);
+    $entity_body_with_a_translations = $this->serializer->encode($normalized_entity, static::$format);
+
+    $request_options = [];
+    $request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType;
+    $request_options[RequestOptions::BODY] = $entity_body_with_a_translations;
+    $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions('POST'));
+
+    $url = $this->getEntityResourcePostUrl();
+    $url->setOption('query', ['_format' => static::$format]);
+
+    // 201 for well-formed request.
+    $response = $this->request('POST', $url, $request_options);
+    $this->assertResourceResponse(201, FALSE, $response);
+    /** @var \Drupal\node\NodeInterface $created_node */
+    $created_node = $this->serializer->deserialize((string) $response->getBody(), get_class($this->entity), static::$format);
+    $this->assertSame([$created_node->toUrl('canonical')->setAbsolute(TRUE)->toString()], $response->getHeader('Location'));
+    $this->assertFalse($response->hasHeader('X-Drupal-Cache'));
+
+    $this->assertSame('Llama', $created_node->getTitle());
+    $this->assertSame('Lama', $created_node->getTranslation('de')->getTitle());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function testPatch() {
+    parent::testPatch();
+
+    $title_with_translation = $this->getNormalizedPatchEntity();
+    $title_with_translation['title'][0]['lang'] = 'en';
+    $title_with_translation['title'][1] = [
+      'value' => 'Drama-Lama',
+      'lang' => 'de',
+    ];
+
+    $entity_body_title_with_translation = $this->serializer->encode($title_with_translation, static::$format);
+
+    $request_options = [];
+    $request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType;
+    $request_options[RequestOptions::BODY] = $entity_body_title_with_translation;
+    $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions('POST'));
+
+    $url = $this->getUrl();
+    $url->setOption('query', ['_format' => static::$format]);
+
+    $response = $this->request('PATCH', $url, $request_options);
+    $this->assertResourceResponse(200, FALSE, $response);
+    $this->assertFalse($response->hasHeader('X-Drupal-Cache'));
+
+    $storage = \Drupal::entityTypeManager()->getStorage('node');
+    $storage->resetCache();
+
+    $patched_node = $storage->load($this->entity->id());
+    $this->assertSame('Dramallama', $patched_node->getTitle());
+    $this->assertSame('Drama-Lama', $patched_node->getTranslation('de')->getTitle());
+  }
+
+}
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
index 1087468..610bfd5 100644
--- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
@@ -6,6 +6,7 @@
 use Drupal\Component\Plugin\PluginManagerInterface;
 use Drupal\Core\Cache\CacheableResponseInterface;
 use Drupal\Core\Config\Entity\ConfigEntityType;
+use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Entity\FieldableEntityInterface;
 use Drupal\Core\Config\ConfigFactoryInterface;
@@ -180,31 +181,26 @@ public function post(EntityInterface $entity = NULL) {
 
     // Validate the received data before saving.
     $this->validate($entity);
-    try {
-      $entity->save();
-      $this->logger->notice('Created entity %type with ID %id.', ['%type' => $entity->getEntityTypeId(), '%id' => $entity->id()]);
-
-      // 201 Created responses return the newly created entity in the response
-      // body. These responses are not cacheable, so we add no cacheability
-      // metadata here.
-      $headers = [];
-      if (in_array('canonical', $entity->uriRelationships(), TRUE)) {
-        $url = $entity->urlInfo('canonical', ['absolute' => TRUE])->toString(TRUE);
-        $headers['Location'] = $url->getGeneratedUrl();
-      }
-      return new ModifiedResourceResponse($entity, 201, $headers);
-    }
-    catch (EntityStorageException $e) {
-      throw new HttpException(500, 'Internal Server Error', $e);
+    $entity->save();
+    $this->logger->notice('Created entity %type with ID %id.', ['%type' => $entity->getEntityTypeId(), '%id' => $entity->id()]);
+
+    // 201 Created responses return the newly created entity in the response
+    // body. These responses are not cacheable, so we add no cacheability
+    // metadata here.
+    $headers = [];
+    if (in_array('canonical', $entity->uriRelationships(), TRUE)) {
+      $url = $entity->urlInfo('canonical', ['absolute' => TRUE])->toString(TRUE);
+      $headers['Location'] = $url->getGeneratedUrl();
     }
+    return new ModifiedResourceResponse($entity, 201, $headers);
   }
 
   /**
    * Responds to entity PATCH requests.
    *
-   * @param \Drupal\Core\Entity\EntityInterface $original_entity
+   * @param \Drupal\Core\Entity\ContentEntityInterface $original_entity
    *   The original entity object.
-   * @param \Drupal\Core\Entity\EntityInterface $entity
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
    *   The entity.
    *
    * @return \Drupal\rest\ModifiedResourceResponse
@@ -212,7 +208,7 @@ public function post(EntityInterface $entity = NULL) {
    *
    * @throws \Symfony\Component\HttpKernel\Exception\HttpException
    */
-  public function patch(EntityInterface $original_entity, EntityInterface $entity = NULL) {
+  public function patch(ContentEntityInterface $original_entity, ContentEntityInterface $entity = NULL) {
     if ($entity == NULL) {
       throw new BadRequestHttpException('No entity content received.');
     }
@@ -227,41 +223,41 @@ public function patch(EntityInterface $original_entity, EntityInterface $entity
 
     // Overwrite the received fields.
     foreach ($entity->_restSubmittedFields as $field_name) {
-      $field = $entity->get($field_name);
-      $original_field = $original_entity->get($field_name);
-
-      // If the user has access to view the field, we need to check update
-      // access regardless of the field value to avoid information disclosure.
-      // (Otherwise the user may try PATCHing with value after value, until they
-      // send the current value for the field, and then they won't get a 403
-      // response anymore, which indicates that the value they sent in the PATCH
-      // request body matches the current value.)
-      if (!$original_field->access('view')) {
-        if (!$original_field->access('edit')) {
+      foreach ($entity->getTranslationLanguages() as $langcode => $language) {
+        if (!$original_entity->hasTranslation($langcode)) {
+          $original_entity->addTranslation($langcode, $original_entity->toArray());
+        }
+        $field = $entity->getTranslation($langcode)->get($field_name);
+        $original_field = $original_entity->getTranslation($langcode)->get($field_name);
+
+        // If the user has access to view the field, we need to check update
+        // access regardless of the field value to avoid information disclosure.
+        // (Otherwise the user may try PATCHing with value after value, until they
+        // send the current value for the field, and then they won't get a 403
+        // response anymore, which indicates that the value they sent in the PATCH
+        // request body matches the current value.)
+        if (!$original_field->access('view')) {
+          if (!$original_field->access('edit')) {
+            throw new AccessDeniedHttpException("Access denied on updating field '$field_name'.");
+          }
+        }
+        // Check access for all received fields, but only if they are being
+        // changed. The bundle of an entity, for example, must be provided for
+        // denormalization to succeed, but it may not be changed.
+        elseif (!$original_field->equals($field) && !$original_field->access('edit')) {
           throw new AccessDeniedHttpException("Access denied on updating field '$field_name'.");
         }
+        $original_entity->getTranslation($langcode)->set($field_name, $field->getValue());
       }
-      // Check access for all received fields, but only if they are being
-      // changed. The bundle of an entity, for example, must be provided for
-      // denormalization to succeed, but it may not be changed.
-      elseif (!$original_field->equals($field) && !$original_field->access('edit')) {
-        throw new AccessDeniedHttpException("Access denied on updating field '$field_name'.");
-      }
-      $original_entity->set($field_name, $field->getValue());
     }
 
     // Validate the received data before saving.
     $this->validate($original_entity);
-    try {
-      $original_entity->save();
-      $this->logger->notice('Updated entity %type with ID %id.', ['%type' => $original_entity->getEntityTypeId(), '%id' => $original_entity->id()]);
+    $original_entity->save();
+    $this->logger->notice('Updated entity %type with ID %id.', ['%type' => $original_entity->getEntityTypeId(), '%id' => $original_entity->id()]);
 
-      // Return the updated entity in the response body.
-      return new ModifiedResourceResponse($original_entity, 200);
-    }
-    catch (EntityStorageException $e) {
-      throw new HttpException(500, 'Internal Server Error', $e);
-    }
+    // Return the updated entity in the response body.
+    return new ModifiedResourceResponse($original_entity, 200);
   }
 
   /**
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php b/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php
index 09b4b64..a769b6b 100644
--- a/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php
@@ -3,7 +3,7 @@
 namespace Drupal\rest\Plugin\rest\resource;
 
 use Drupal\Component\Render\PlainTextOutput;
-use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Entity\FieldableEntityInterface;
 use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
 
@@ -16,31 +16,31 @@
   /**
    * Verifies that the whole entity does not violate any validation constraints.
    *
-   * @param \Drupal\Core\Entity\EntityInterface $entity
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
    *   The entity to validate.
    *
    * @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException
    *   If validation errors are found.
    */
-  protected function validate(EntityInterface $entity) {
+  protected function validate(ContentEntityInterface $entity) {
     // @todo Remove when https://www.drupal.org/node/2164373 is committed.
     if (!$entity instanceof FieldableEntityInterface) {
       return;
     }
-    $violations = $entity->validate();
-
-    // Remove violations of inaccessible fields as they cannot stem from our
-    // changes.
-    $violations->filterByFieldAccess();
-
-    if ($violations->count() > 0) {
-      $message = "Unprocessable Entity: validation failed.\n";
-      foreach ($violations as $violation) {
-        // We strip every HTML from the error message to have a nicer to read
-        // message on REST responses.
-        $message .= $violation->getPropertyPath() . ': ' . PlainTextOutput::renderFromHtml($violation->getMessage()) . "\n";
+    foreach ($entity->getTranslationLanguages() as $langcode => $language) {
+      $violations = $entity->getTranslation($langcode)->validate();
+      // Remove violations of inaccessible fields as they cannot stem from our
+      // changes.
+      $violations->filterByFieldAccess();
+      if ($violations->count() > 0) {
+        $message = "Unprocessable Entity: validation failed.\n";
+        foreach ($violations as $violation) {
+          // We strip every HTML from the error message to have a nicer to read
+          // message on REST responses.
+          $message .= $violation->getPropertyPath() . ': ' . PlainTextOutput::renderFromHtml($violation->getMessage()) . "\n";
+        }
+        throw new UnprocessableEntityHttpException($message);
       }
-      throw new UnprocessableEntityHttpException($message);
     }
   }
 
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php
index 5b51014..f3bd70f 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php
@@ -278,7 +278,7 @@ public function testPostDxWithoutCriticalBaseFields() {
     // @todo Uncomment, remove next 3 lines in https://www.drupal.org/node/2820364.
     $this->assertSame(500, $response->getStatusCode());
     $this->assertSame(['text/plain; charset=UTF-8'], $response->getHeader('Content-Type'));
-    $this->assertStringStartsWith('The website encountered an unexpected error. Please try again later.</br></br><em class="placeholder">Symfony\Component\HttpKernel\Exception\HttpException</em>: Internal Server Error in <em class="placeholder">Drupal\rest\Plugin\rest\resource\EntityResource-&gt;post()</em>', (string) $response->getBody());
+    $this->assertStringStartsWith('The website encountered an unexpected error. Please try again later.</br></br><em class="placeholder">Drupal\Core\Entity\EntityStorageException</em>: The &quot;&quot; entity type does not exist. in <em class="placeholder">Drupal\Core\Entity\Sql\SqlContentEntityStorage-&gt;save()</em>', (string) $response->getBody());
     //$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nentity_type: This value should not be null.\n", $response);
 
     // DX: 422 when missing 'entity_id' field.
diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
index 72589bd..5753c85 100644
--- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
+++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php
@@ -473,12 +473,16 @@ public function testGet() {
     if ($this->entity->getEntityType()->getLinkTemplates()) {
       $this->assertArrayHasKey('Link', $response->getHeaders());
       $link_relation_type_manager = $this->container->get('plugin.manager.link_relation_type');
-      $expected_link_relation_headers = array_map(function ($relation_name) use ($link_relation_type_manager) {
+      $expected_link_relation_headers = array_filter(array_map(function ($relation_name) use ($link_relation_type_manager) {
+        // @todo What to do about content_translation link relationships?
+        if (strpos($relation_name, 'drupal:content-translation') !== FALSE) {
+          return FALSE;
+        }
         $link_relation_type = $link_relation_type_manager->createInstance($relation_name);
         return $link_relation_type->isRegistered()
           ? $link_relation_type->getRegisteredName()
           : $link_relation_type->getExtensionUri();
-      }, array_keys($this->entity->getEntityType()->getLinkTemplates()));
+      }, array_keys($this->entity->getEntityType()->getLinkTemplates())));
       $parse_rel_from_link_header = function ($value) use ($link_relation_type_manager) {
         $matches = [];
         if (preg_match('/rel="([^"]+)"/', $value, $matches) === 1) {
diff --git a/core/modules/rest/tests/src/Unit/EntityResourceValidationTraitTest.php b/core/modules/rest/tests/src/Unit/EntityResourceValidationTraitTest.php
index 8c9758e..4683277 100644
--- a/core/modules/rest/tests/src/Unit/EntityResourceValidationTraitTest.php
+++ b/core/modules/rest/tests/src/Unit/EntityResourceValidationTraitTest.php
@@ -3,6 +3,7 @@
 namespace Drupal\Tests\rest\Unit;
 
 use Drupal\Core\Entity\EntityConstraintViolationList;
+use Drupal\Core\Language\LanguageInterface;
 use Drupal\node\Entity\Node;
 use Drupal\Tests\UnitTestCase;
 use Drupal\user\Entity\User;
@@ -28,8 +29,12 @@ public function testValidate() {
     $violations->filterByFieldAccess()->shouldBeCalled()->willReturn([]);
     $violations->count()->shouldBeCalled()->willReturn(0);
 
+    $translatable_entity = $this->prophesize(Node::class);
+    $translatable_entity->validate()->shouldBeCalled()->willReturn($violations->reveal());
+
     $entity = $this->prophesize(Node::class);
-    $entity->validate()->shouldBeCalled()->willReturn($violations->reveal());
+    $entity->getTranslationLanguages()->shouldBeCalled()->willReturn(['en' => LanguageInterface::class]);
+    $entity->getTranslation('en')->shouldBeCalled()->willReturn($translatable_entity);
 
     $method->invoke($trait, $entity->reveal());
   }
@@ -57,7 +62,11 @@ public function testFailedValidate() {
       ->method('filterByFieldAccess')
       ->will($this->returnValue([]));
 
-    $entity->validate()->willReturn($violations);
+    $translatable_entity = $this->prophesize(Node::class);
+    $translatable_entity->validate()->shouldBeCalled()->willReturn($violations);
+
+    $entity->getTranslationLanguages()->shouldBeCalled()->willReturn(['en' => LanguageInterface::class]);
+    $entity->getTranslation('en')->shouldBeCalled()->willReturn($translatable_entity);
 
     $trait = $this->getMockForTrait('Drupal\rest\Plugin\rest\resource\EntityResourceValidationTrait');
 
