diff --git a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
index 6d10b06f32..6dd2f8e1c7 100644
--- a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
+++ b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
@@ -44,17 +44,6 @@ public function denormalize($data, $class, $format = NULL, array $context = [])
     }
 
     $field_item = $context['target_instance'];
-
-    // If this field is translatable, we need to create a translated instance.
-    if (isset($data['lang'])) {
-      $langcode = $data['lang'];
-      unset($data['lang']);
-      $field_definition = $field_item->getFieldDefinition();
-      if ($field_definition->isTranslatable()) {
-        $field_item = $this->createTranslatedInstance($field_item, $langcode);
-      }
-    }
-
     $field_item->setValue($this->constructValue($data, $context));
     return $field_item;
   }
@@ -103,34 +92,4 @@ protected function normalizedFieldValues(FieldItemInterface $field_item, $format
     return $normalized;
   }
 
-  /**
-   * Get a translated version of the field item instance.
-   *
-   * To indicate that a field item applies to one translation of an entity and
-   * not another, the property path must originate with a translation of the
-   * entity. This is the reason for using target_instances, from which the
-   * property path can be traversed up to the root.
-   *
-   * @param \Drupal\Core\Field\FieldItemInterface $item
-   *   The untranslated field item instance.
-   * @param $langcode
-   *   The langcode.
-   *
-   * @return \Drupal\Core\Field\FieldItemInterface
-   *   The translated field item instance.
-   */
-  protected function createTranslatedInstance(FieldItemInterface $item, $langcode) {
-    // Remove the untranslated item that was created for the default language
-    // by FieldNormalizer::denormalize().
-    $items = $item->getParent();
-    $delta = $item->getName();
-    unset($items[$delta]);
-
-    // Instead, create a new item for the entity in the requested language.
-    $entity = $item->getEntity();
-    $entity_translation = $entity->hasTranslation($langcode) ? $entity->getTranslation($langcode) : $entity->addTranslation($langcode);
-    $field_name = $item->getFieldDefinition()->getName();
-    return $entity_translation->get($field_name)->appendItem();
-  }
-
 }
diff --git a/core/modules/hal/src/Normalizer/FieldNormalizer.php b/core/modules/hal/src/Normalizer/FieldNormalizer.php
index 1ac987c1ef..abea9672a9 100644
--- a/core/modules/hal/src/Normalizer/FieldNormalizer.php
+++ b/core/modules/hal/src/Normalizer/FieldNormalizer.php
@@ -3,7 +3,11 @@
 namespace Drupal\hal\Normalizer;
 
 use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\TypedData\TranslatableInterface;
 use Drupal\serialization\Normalizer\FieldNormalizer as SerializationFieldNormalizer;
+use Symfony\Component\Serializer\Exception\InvalidArgumentException;
+use Symfony\Component\Serializer\Exception\UnexpectedValueException;
 
 /**
  * Converts the Drupal field structure to HAL array structure.
@@ -48,6 +52,72 @@ public function normalize($field_items, $format = NULL, array $context = []) {
     return NestedArray::mergeDeepArray($normalized_field_items);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function denormalize($data, $class, $format = NULL, array $context = []) {
+    if (!isset($context['target_instance'])) {
+      throw new InvalidArgumentException('$context[\'target_instance\'] must be set to denormalize with the FieldNormalizer');
+    }
+
+    if ($context['target_instance']->getParent() == NULL) {
+      throw new InvalidArgumentException('The field passed in via $context[\'target_instance\'] must have a parent set.');
+    }
+
+    /** @var \Drupal\Core\Field\FieldItemListInterface $items */
+    $items = $context['target_instance'];
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+    $entity = $items->getEntity();
+    $field_definition = $items->getFieldDefinition();
+    $item_class = $items->getItemDefinition()->getClass();
+    $original_langcode = $items->getLangcode();
+
+    if (!is_array($data)) {
+      throw new UnexpectedValueException(sprintf('Field values for "%s" must use an array structure', $items->getName()));
+    }
+
+    foreach ($data as $item_data) {
+      if ($field_definition->isTranslatable()) {
+        // For translatable fields all field items for all translations are on
+        // the same level so we must split them them up according to the 'lang'
+        // key and assign the denormalized field items to the correct
+        // entity translation.
+        if (isset($item_data['lang'])) {
+          if (!$entity->hasTranslation($item_data['lang'])) {
+            // Create an entity translation without applying default values.
+            // @see \Drupal\Core\Entity\ContentEntityStorageBase::createTranslation()
+            // @see \Drupal\Core\Entity\ContentEntityStorageBase::initFieldValues()
+            // @TODO Not sure what to do about the 'langcode' and
+            //   'default_langcode' key fields?!
+            // @TODO Should this part maybe even move to
+            //   ContentEntityNormalizer?
+            $translatable_fields = array_keys(array_filter($entity->getFieldDefinitions(), function (FieldDefinitionInterface $definition) {
+              return $definition->isTranslatable();
+            }));
+            $values = array_fill_keys($translatable_fields, []);
+            $entity->addTranslation($item_data['lang'], $values);
+          }
+          $translation = $entity->getTranslation($item_data['lang']);
+        }
+        // If a field is translatable, but this field item does not specify a
+        // language code, add the field item to the default translation.
+        else {
+          $translation = $entity->getTranslation($original_langcode);
+        }
+
+        $items = $translation->get($field_definition->getName());
+      }
+
+      // Create a new item and pass it as the target for the unserialization of
+      // $item_data. All items in field should have removed before this method
+      // was called.
+      // @see \Drupal\serialization\Normalizer\ContentEntityNormalizer::denormalize().
+      $context['target_instance'] = $items->appendItem();
+      $this->serializer->denormalize($item_data, $item_class, $format, $context);
+    }
+    return $items;
+  }
+
   /**
    * Helper function to normalize field items.
    *
diff --git a/core/modules/hal/tests/src/Kernel/TranslationNormalizeTest.php b/core/modules/hal/tests/src/Kernel/TranslationNormalizeTest.php
new file mode 100644
index 0000000000..bb0eb5276d
--- /dev/null
+++ b/core/modules/hal/tests/src/Kernel/TranslationNormalizeTest.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Drupal\Tests\hal\Kernel;
+
+use Drupal\entity_test\Entity\EntityTestMulChanged;
+
+/**
+ * Test normalizing and denormalizing an entity with a translation.
+ *
+ * @group hal
+ */
+class TranslationNormalizeTest extends NormalizerTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $entityClass = 'Drupal\entity_test\Entity\EntityTestMulChanged';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->installEntitySchema('entity_test_mul_changed');
+
+  }
+
+  /**
+   * Tests normalizing and denormalizing an entity.
+   */
+  public function testTranslationNormalize() {
+    $target_entity = EntityTestMulChanged::create((['langcode' => 'en', 'field_test_entity_reference' => NULL]));
+    $target_entity->save();
+
+    $target_entity->addTranslation('de', $target_entity->toArray());
+
+    $this->assertEquals(1, count($target_entity->changed));
+    $this->assertEquals(1, count($target_entity->getTranslation('en')->changed));
+    $this->assertEquals(1, count($target_entity->getTranslation('de')->changed));
+
+    $data = $this->serializer->normalize($target_entity, 'hal_json');
+    $denormalized_entity = $this->serializer->denormalize($data, $this->entityClass, 'hal_json');
+
+    $this->assertEquals(1, count($denormalized_entity->changed));
+    $this->assertEquals(1, count($target_entity->getTranslation('en')->changed));
+    $this->assertEquals(1, count($denormalized_entity->getTranslation('de')->changed));
+  }
+
+}
