diff --git a/core/modules/content_translation/src/ContentTranslationUpdatesManager.php b/core/modules/content_translation/src/ContentTranslationUpdatesManager.php
index 8b34797..4fa54f3 100644
--- a/core/modules/content_translation/src/ContentTranslationUpdatesManager.php
+++ b/core/modules/content_translation/src/ContentTranslationUpdatesManager.php
@@ -2,10 +2,13 @@
 
 namespace Drupal\content_translation;
 
+use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Config\ConfigEvents;
 use Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\migrate\Event\MigrateEvents;
+use Drupal\migrate\Event\MigrateImportEvent;
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
 
 /**
@@ -75,10 +78,26 @@ public function onConfigImporterImport() {
   }
 
   /**
+   * Listener for migration imports.
+   */
+  public function onMigrateImport(MigrateImportEvent $event) {
+    $migration = $event->getMigration();
+    $configuration = $migration->getDestinationConfiguration();
+    $entity_types = NestedArray::getValue($configuration, ['content_translation_update_definitions']);
+    if ($entity_types) {
+      $entity_types = array_intersect_key($this->entityManager->getDefinitions(), array_flip($entity_types));
+      $this->updateDefinitions($entity_types);
+    }
+  }
+
+  /**
    * {@inheritdoc}
    */
   public static function getSubscribedEvents() {
     $events[ConfigEvents::IMPORT][] = ['onConfigImporterImport', 60];
+    if (class_exists('\Drupal\migrate\Event\MigrateEvents')) {
+      $events[MigrateEvents::POST_IMPORT][] = ['onMigrateImport'];
+    }
     return $events;
   }
 
diff --git a/core/modules/language/migration_templates/d6_language_content_settings.yml b/core/modules/language/migration_templates/d6_language_content_settings.yml
index e5dc750..3bf9078 100644
--- a/core/modules/language/migration_templates/d6_language_content_settings.yml
+++ b/core/modules/language/migration_templates/d6_language_content_settings.yml
@@ -1,5 +1,6 @@
 id: d6_language_content_settings
 label: Drupal 6 language content settings
+
 migration_tags:
   - Drupal 6
 source:
@@ -39,6 +40,8 @@ process:
       2: true
 destination:
   plugin: entity:language_content_settings
+  content_translation_update_definitions:
+    - node
 migration_dependencies:
   required:
     - d6_node_type
diff --git a/core/modules/language/migration_templates/d7_language_content_settings.yml b/core/modules/language/migration_templates/d7_language_content_settings.yml
index cbe935a..09437fa 100644
--- a/core/modules/language/migration_templates/d7_language_content_settings.yml
+++ b/core/modules/language/migration_templates/d7_language_content_settings.yml
@@ -39,6 +39,8 @@ process:
       2: true
 destination:
   plugin: entity:language_content_settings
+  content_translation_update_definitions:
+    - node
 migration_dependencies:
   required:
     - d7_node_type
diff --git a/core/modules/migrate/src/Plugin/migrate/destination/DestinationBase.php b/core/modules/migrate/src/Plugin/migrate/destination/DestinationBase.php
index b4cf30b..18a95e6 100644
--- a/core/modules/migrate/src/Plugin/migrate/destination/DestinationBase.php
+++ b/core/modules/migrate/src/Plugin/migrate/destination/DestinationBase.php
@@ -94,8 +94,10 @@ public function supportsRollback() {
    *
    * @param array $id_map
    *   The map row data for the item.
+   * @param int $update_action
+   *   The rollback action to take if we are updating an existing item.
    */
-  protected function setRollbackAction(array $id_map) {
+  protected function setRollbackAction(array $id_map, $update_action = MigrateIdMapInterface::ROLLBACK_PRESERVE) {
     // If the entity we're updating was previously migrated by us, preserve the
     // existing rollback action.
     if (isset($id_map['sourceid1'])) {
@@ -104,7 +106,7 @@ protected function setRollbackAction(array $id_map) {
     // Otherwise, we're updating an entity which already existed on the
     // destination and want to make sure we do not delete it on rollback.
     else {
-      $this->rollbackAction = MigrateIdMapInterface::ROLLBACK_PRESERVE;
+      $this->rollbackAction = $update_action;
     }
   }
 
diff --git a/core/modules/migrate/src/Plugin/migrate/destination/Entity.php b/core/modules/migrate/src/Plugin/migrate/destination/Entity.php
index abf2dc9..1b2c2e7 100644
--- a/core/modules/migrate/src/Plugin/migrate/destination/Entity.php
+++ b/core/modules/migrate/src/Plugin/migrate/destination/Entity.php
@@ -124,7 +124,8 @@ public function fields(MigrationInterface $migration = NULL) {
   protected function getEntity(Row $row, array $old_destination_id_values) {
     $entity_id = reset($old_destination_id_values) ?: $this->getEntityId($row);
     if (!empty($entity_id) && ($entity = $this->storage->load($entity_id))) {
-      $this->updateEntity($entity, $row);
+      // Allow updateEntity() to change the entity.
+      $entity = $this->updateEntity($entity, $row) ?: $entity;
     }
     else {
       // Attempt to ensure we always have a bundle.
diff --git a/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php b/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php
index 6a2fb98..3617f38 100644
--- a/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php
+++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php
@@ -7,6 +7,7 @@
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Field\FieldTypePluginManagerInterface;
+use Drupal\Core\TypedData\TranslatableInterface;
 use Drupal\Core\TypedData\TypedDataInterface;
 use Drupal\migrate\Plugin\MigrationInterface;
 use Drupal\migrate\MigrateException;
@@ -85,7 +86,12 @@ public function import(Row $row, array $old_destination_id_values = array()) {
     if (!$entity) {
       throw new MigrateException('Unable to get entity');
     }
-    return $this->save($entity, $old_destination_id_values);
+
+    $ids = $this->save($entity, $old_destination_id_values);
+    if (!empty($this->configuration['translations'])) {
+      $ids[] = $entity->language()->getId();
+    }
+    return $ids;
   }
 
   /**
@@ -105,11 +111,31 @@ protected function save(ContentEntityInterface $entity, array $old_destination_i
   }
 
   /**
+   * Get whether this destination is for translations.
+   *
+   * @return bool
+   *   Whether this destination is for translations.
+   */
+  protected function isTranslationDestination() {
+    return !empty($this->configuration['translations']);
+  }
+
+  /**
    * {@inheritdoc}
    */
   public function getIds() {
     $id_key = $this->getKey('id');
     $ids[$id_key]['type'] = 'integer';
+
+    if ($this->isTranslationDestination()) {
+      if ($key = $this->getKey('langcode')) {
+        $ids[$key]['type'] = 'string';
+      }
+      else {
+        throw new MigrateException('This entity type does not support translation.');
+      }
+    }
+
     return $ids;
   }
 
@@ -120,8 +146,29 @@ public function getIds() {
    *   The entity to update.
    * @param \Drupal\migrate\Row $row
    *   The row object to update from.
+   *
+   * @return NULL|\Drupal\Core\Entity\EntityInterface
+   *   An updated entity, or NULL if it's the same as the one passed in.
    */
   protected function updateEntity(EntityInterface $entity, Row $row) {
+    // By default, an update will be preserved.
+    $rollback_action = MigrateIdMapInterface::ROLLBACK_PRESERVE;
+
+    // Make sure we have the right translation.
+    if ($this->isTranslationDestination()) {
+      $property = $this->storage->getEntityType()->getKey('langcode');
+      if ($row->hasDestinationProperty($property)) {
+        $language = $row->getDestinationProperty($property);
+        if (!$entity->hasTranslation($language)) {
+          $entity->addTranslation($language);
+
+          // We're adding a translation, so delete it on rollback.
+          $rollback_action = MigrateIdMapInterface::ROLLBACK_DELETE;
+        }
+        $entity = $entity->getTranslation($language);
+      }
+    }
+
     // If the migration has specified a list of properties to be overwritten,
     // clone the row with an empty set of destination values, and re-add only
     // the specified properties.
@@ -140,7 +187,10 @@ protected function updateEntity(EntityInterface $entity, Row $row) {
       }
     }
 
-    $this->setRollbackAction($row->getIdMap());
+    $this->setRollbackAction($row->getIdMap(), $rollback_action);
+
+    // We might have a different (translated) entity, so return it.
+    return $entity;
   }
 
   /**
@@ -185,4 +235,32 @@ protected function processStubRow(Row $row) {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function rollback(array $destination_identifier) {
+    if ($this->isTranslationDestination()) {
+      // Attempt to remove the translation.
+      $entity = $this->storage->load(reset($destination_identifier));
+      if ($entity && $entity instanceof TranslatableInterface) {
+        if ($key = $this->getKey('langcode')) {
+          if (isset($destination_identifier[$key])) {
+            $langcode = $destination_identifier[$key];
+            if ($entity->hasTranslation($langcode)) {
+              // Make sure we don't remove the default translation.
+              $translation = $entity->getTranslation($langcode);
+              if (!$translation->isDefaultTranslation()) {
+                $entity->removeTranslation($langcode);
+                $entity->save();
+              }
+            }
+          }
+        }
+      }
+    }
+    else {
+      parent::rollback($destination_identifier);
+    }
+  }
+
 }
diff --git a/core/modules/migrate/tests/modules/migrate_external_translated_test/migrate_external_translated_test.info.yml b/core/modules/migrate/tests/modules/migrate_external_translated_test/migrate_external_translated_test.info.yml
new file mode 100644
index 0000000..065d512
--- /dev/null
+++ b/core/modules/migrate/tests/modules/migrate_external_translated_test/migrate_external_translated_test.info.yml
@@ -0,0 +1,8 @@
+name: 'Migration external translated test'
+type: module
+package: Testing
+version: VERSION
+core: 8.x
+dependencies:
+  - node
+  - migrate
diff --git a/core/modules/migrate/tests/modules/migrate_external_translated_test/migrations/migrate.migration.external_translated_test_node.yml b/core/modules/migrate/tests/modules/migrate_external_translated_test/migrations/migrate.migration.external_translated_test_node.yml
new file mode 100644
index 0000000..f643b60
--- /dev/null
+++ b/core/modules/migrate/tests/modules/migrate_external_translated_test/migrations/migrate.migration.external_translated_test_node.yml
@@ -0,0 +1,19 @@
+id: external_translated_test_node
+label: External translated content
+source:
+  plugin: migrate_external_translated_test
+  default_lang: true
+  constants:
+    type: external_test
+process:
+  type: constants/type
+  title: title
+  langcode:
+    plugin: static_map
+    source: lang
+    map:
+      English: en
+      French: fr
+      Spanish: es
+destination:
+  plugin: entity:node
diff --git a/core/modules/migrate/tests/modules/migrate_external_translated_test/migrations/migrate.migration.external_translated_test_node_translation.yml b/core/modules/migrate/tests/modules/migrate_external_translated_test/migrations/migrate.migration.external_translated_test_node_translation.yml
new file mode 100644
index 0000000..ff29084
--- /dev/null
+++ b/core/modules/migrate/tests/modules/migrate_external_translated_test/migrations/migrate.migration.external_translated_test_node_translation.yml
@@ -0,0 +1,27 @@
+id: external_translated_test_node_translation
+label: External translated content translations
+source:
+  plugin: migrate_external_translated_test
+  default_lang: false
+  constants:
+    type: external_test
+process:
+  nid:
+    plugin: migration
+    source: name
+    migration: external_translated_test_node
+  type: constants/type
+  title: title
+  langcode:
+    plugin: static_map
+    source: lang
+    map:
+      English: en
+      French: fr
+      Spanish: es
+destination:
+  plugin: entity:node
+  translations: true
+migration_dependencies:
+  required:
+    - external_translated_test_node
diff --git a/core/modules/migrate/tests/modules/migrate_external_translated_test/src/Plugin/migrate/source/MigrateExternalTranslatedTestSource.php b/core/modules/migrate/tests/modules/migrate_external_translated_test/src/Plugin/migrate/source/MigrateExternalTranslatedTestSource.php
new file mode 100644
index 0000000..ceda6d8
--- /dev/null
+++ b/core/modules/migrate/tests/modules/migrate_external_translated_test/src/Plugin/migrate/source/MigrateExternalTranslatedTestSource.php
@@ -0,0 +1,77 @@
+<?php
+
+namespace Drupal\migrate_external_translated_test\Plugin\migrate\source;
+
+use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
+
+/**
+ * A simple migrate source for our tests.
+ *
+ * @MigrateSource(
+ *   id = "migrate_external_translated_test"
+ * )
+ */
+class MigrateExternalTranslatedTestSource extends SourcePluginBase {
+
+  /**
+   * The data to import.
+   *
+   * @var array
+   */
+  protected $import = [
+    ['name' => 'cat', 'title' => 'Cat', 'lang' => 'English'],
+    ['name' => 'cat', 'title' => 'Chat', 'lang' => 'French'],
+    ['name' => 'cat', 'title' => 'Gato', 'lang' => 'Spanish'],
+    ['name' => 'dog', 'title' => 'Dog', 'lang' => 'English'],
+    ['name' => 'dog', 'title' => 'Chien', 'lang' => 'French'],
+    ['name' => 'monkey', 'title' => 'Monkey', 'lang' => 'English'],
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fields() {
+    return [
+      'name' => $this->t('Unique name'),
+      'title' => $this->t('Title'),
+      'lang' => $this->t('Language'),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __toString() {
+    return '';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIds() {
+    $ids['name']['type'] = 'string';
+    if (!$this->configuration['default_lang']) {
+      $ids['lang']['type'] = 'string';
+    }
+    return $ids;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function initializeIterator() {
+    $data = [];
+
+    // Keep the rows with the right languages.
+    $want_default = $this->configuration['default_lang'];
+    foreach ($this->import as $row) {
+      $is_english = $row['lang'] == 'English';
+      if ($want_default == $is_english) {
+        $data[] = $row;
+      }
+    }
+
+    return new \ArrayIterator($data);
+  }
+
+}
diff --git a/core/modules/migrate/tests/src/Kernel/MigrateEntityContentBaseTest.php b/core/modules/migrate/tests/src/Kernel/MigrateEntityContentBaseTest.php
new file mode 100644
index 0000000..352b660
--- /dev/null
+++ b/core/modules/migrate/tests/src/Kernel/MigrateEntityContentBaseTest.php
@@ -0,0 +1,159 @@
+<?php
+
+namespace Drupal\Tests\migrate\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\migrate\Plugin\migrate\destination\EntityContentBase;
+use Drupal\migrate\Plugin\MigrateIdMapInterface;
+use Drupal\migrate\Plugin\MigrationInterface;
+use Drupal\migrate\Row;
+
+/**
+ * Tests the EntityContentBase destination.
+ *
+ * @group migrate
+ */
+class MigrateEntityContentBaseTest extends KernelTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['migrate', 'user', 'language', 'entity_test'];
+
+  /**
+   * The storage for entity_test_mul.
+   *
+   * @var \Drupal\Core\Entity\ContentEntityStorageInterface
+   */
+  protected $storage;
+
+  /**
+   * A content migrate destination.
+   *
+   * @var \Drupal\migrate\Plugin\MigrateDestinationInterface
+   */
+  protected $destination;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->installEntitySchema('entity_test_mul');
+
+    ConfigurableLanguage::createFromLangcode('en')->save();
+    ConfigurableLanguage::createFromLangcode('fr')->save();
+
+    $this->storage = $this->container->get('entity.manager')->getStorage('entity_test_mul');
+  }
+
+  /**
+   * Check the existing translations of an entity.
+   *
+   * @param int $id
+   *   The entity ID.
+   * @param string $default
+   *   The expected default translation language code.
+   * @param string[] $others
+   *   The expected other translation language codes.
+   */
+  protected function assertTranslations($id, $default, $others = []) {
+    $entity = $this->storage->load($id);
+    $this->assertTrue($entity, "Entity exists");
+    $this->assertEquals($default, $entity->language()->getId(), "Entity default translation");
+    $translations = array_keys($entity->getTranslationLanguages(FALSE));
+    sort($others);
+    sort($translations);
+    $this->assertEquals($others, $translations, "Entity translations");
+  }
+
+  /**
+   * Create the destination plugin to test.
+   *
+   * @param array $configuration
+   *   The plugin configuration.
+   */
+  protected function createDestination(array $configuration) {
+    $this->destination = new EntityContentBase(
+      $configuration,
+      'fake_plugin_id',
+      [],
+      $this->getMock(MigrationInterface::class),
+      $this->storage,
+      [],
+      $this->container->get('entity.manager'),
+      $this->container->get('plugin.manager.field.field_type')
+    );
+  }
+
+  /**
+   * Test importing and rolling back translated entities.
+   */
+  public function testTranslated() {
+    // Create a destination.
+    $this->createDestination(['translations' => TRUE]);
+
+    // Create some pre-existing entities.
+    $this->storage->create(['id' => 1, 'langcode' => 'en'])->save();
+    $this->storage->create(['id' => 2, 'langcode' => 'fr'])->save();
+    $translated = $this->storage->create(['id' => 3, 'langcode' => 'en']);
+    $translated->save();
+    $translated->addTranslation('fr')->save();
+
+    // Pre-assert that things are as expected.
+    $this->assertTranslations(1, 'en');
+    $this->assertTranslations(2, 'fr');
+    $this->assertTranslations(3, 'en', ['fr']);
+    $this->assertFalse($this->storage->load(4));
+
+    $destination_rows = [
+      // Existing default translation.
+      ['id' => 1, 'langcode' => 'en', 'action' => MigrateIdMapInterface::ROLLBACK_PRESERVE],
+      // New translation.
+      ['id' => 2, 'langcode' => 'en', 'action' => MigrateIdMapInterface::ROLLBACK_DELETE],
+      // Existing non-default translation.
+      ['id' => 3, 'langcode' => 'fr', 'action' => MigrateIdMapInterface::ROLLBACK_PRESERVE],
+      // Brand new row.
+      ['id' => 4, 'langcode' => 'fr', 'action' => MigrateIdMapInterface::ROLLBACK_DELETE],
+    ];
+    $rollback_actions = [];
+
+    // Import some rows.
+    foreach ($destination_rows as $idx => $destination_row) {
+      $row = new Row([], []);
+      foreach ($destination_row as $key => $value) {
+        $row->setDestinationProperty($key, $value);
+      }
+      $this->destination->import($row);
+
+      // Check that the rollback action is correct, and save it.
+      $this->assertEquals($destination_row['action'], $this->destination->rollbackAction());
+      $rollback_actions[$idx] = $this->destination->rollbackAction();
+    }
+
+    $this->assertTranslations(1, 'en');
+    $this->assertTranslations(2, 'fr', ['en']);
+    $this->assertTranslations(3, 'en', ['fr']);
+    $this->assertTranslations(4, 'fr');
+
+    // Rollback the rows.
+    foreach ($destination_rows as $idx => $destination_row) {
+      if ($rollback_actions[$idx] == MigrateIdMapInterface::ROLLBACK_DELETE) {
+        $this->destination->rollback($destination_row);
+      }
+    }
+
+    // No change, update of existing translation.
+    $this->assertTranslations(1, 'en');
+    // Remove added translation.
+    $this->assertTranslations(2, 'fr');
+    // No change, update of existing translation.
+    $this->assertTranslations(3, 'en', ['fr']);
+    // No change, can't remove default translation.
+    $this->assertTranslations(4, 'fr');
+  }
+
+}
diff --git a/core/modules/migrate/tests/src/Kernel/MigrateExternalTranslatedTest.php b/core/modules/migrate/tests/src/Kernel/MigrateExternalTranslatedTest.php
new file mode 100644
index 0000000..ce9c013
--- /dev/null
+++ b/core/modules/migrate/tests/src/Kernel/MigrateExternalTranslatedTest.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace Drupal\Tests\migrate\Kernel;
+
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\migrate\MigrateExecutable;
+use Drupal\node\Entity\NodeType;
+
+/**
+ * Tests migrating non-Drupal translated content.
+ *
+ * Ensure it's possible to migrate in translations, even if there's no nid or
+ * tnid property on the source.
+ *
+ * @group migrate
+ */
+class MigrateExternalTranslatedTest extends MigrateTestBase {
+
+  /**
+   * {@inheritdoc}
+   *
+   * @todo: Remove migrate_drupal when https://www.drupal.org/node/2560795 is
+   * fixed.
+   */
+  public static $modules = ['system', 'user', 'language', 'node', 'field', 'migrate_drupal', 'migrate_external_translated_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    $this->installSchema('system', ['sequences']);
+    $this->installSchema('node', array('node_access'));
+    $this->installEntitySchema('user');
+    $this->installEntitySchema('node');
+
+    // Create some languages.
+    ConfigurableLanguage::createFromLangcode('en')->save();
+    ConfigurableLanguage::createFromLangcode('fr')->save();
+    ConfigurableLanguage::createFromLangcode('es')->save();
+
+    // Create a content type.
+    NodeType::create([
+      'type' => 'external_test',
+      'name' => 'Test node type',
+    ])->save();
+  }
+
+  /**
+   * Test importing and rolling back our data.
+   */
+  public function testMigrations() {
+    /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
+    $storage = $this->container->get('entity.manager')->getStorage('node');
+    $this->assertEquals(0, count($storage->loadMultiple()));
+
+    // Run the migrations.
+    $migration_ids = ['external_translated_test_node', 'external_translated_test_node_translation'];
+    $this->executeMigrations($migration_ids);
+    $this->assertEquals(3, count($storage->loadMultiple()));
+
+    $node = $storage->load(1);
+    $this->assertEquals('en', $node->language()->getId());
+    $this->assertEquals('Cat', $node->title->value);
+    $this->assertEquals('Chat', $node->getTranslation('fr')->title->value);
+    $this->assertEquals('Gato', $node->getTranslation('es')->title->value);
+
+    $node = $storage->load(2);
+    $this->assertEquals('en', $node->language()->getId());
+    $this->assertEquals('Dog', $node->title->value);
+    $this->assertEquals('Chien', $node->getTranslation('fr')->title->value);
+    $this->assertFalse($node->hasTranslation('es'), "No spanish translation for node 2");
+
+    $node = $storage->load(3);
+    $this->assertEquals('en', $node->language()->getId());
+    $this->assertEquals('Monkey', $node->title->value);
+    $this->assertFalse($node->hasTranslation('fr'), "No french translation for node 3");
+    $this->assertFalse($node->hasTranslation('es'), "No spanish translation for node 3");
+
+    $this->assertNull($storage->load(4), "No node 4 migrated");
+
+    // Roll back the migrations.
+    foreach ($migration_ids as $migration_id) {
+      $migration = $this->getMigration($migration_id);
+      $executable = new MigrateExecutable($migration, $this);
+      $executable->rollback();
+    }
+
+    $this->assertEquals(0, count($storage->loadMultiple()));
+  }
+
+}
diff --git a/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityContentBaseTest.php b/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityContentBaseTest.php
index da20b49..33aec58 100644
--- a/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityContentBaseTest.php
+++ b/core/modules/migrate/tests/src/Unit/Plugin/migrate/destination/EntityContentBaseTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\Tests\migrate\Unit\Plugin\migrate\destination;
 
 use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\ContentEntityType;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Field\FieldTypePluginManagerInterface;
@@ -97,6 +98,33 @@ public function testImportEntityLoadFailure() {
     $destination->import(new Row([], []));
   }
 
+  /**
+   * Test that translation destination fails for untranslatable entities.
+   *
+   * @expectedException \Drupal\migrate\MigrateException
+   * @expectedExceptionMessage This entity type does not support translation
+   */
+  public function testUntranslatable() {
+    // An entity type without a language.
+    $entity_type = $this->prophesize(ContentEntityType::class);
+    $entity_type->getKey('langcode')->willReturn('');
+    $entity_type->getKey('id')->willReturn('id');
+
+    $this->storage->getEntityType()->willReturn($entity_type->reveal());
+
+    $destination = new EntityTestDestination(
+      [ 'translations' => TRUE ],
+      '',
+      [],
+      $this->migration->reveal(),
+      $this->storage->reveal(),
+      [],
+      $this->entityManager->reveal(),
+      $this->prophesize(FieldTypePluginManagerInterface::class)->reveal()
+    );
+    $destination->getIds();
+  }
+
 }
 
 /**
diff --git a/core/modules/migrate_drupal/tests/fixtures/drupal6.php b/core/modules/migrate_drupal/tests/fixtures/drupal6.php
index 97ed43e..bc39135 100644
--- a/core/modules/migrate_drupal/tests/fixtures/drupal6.php
+++ b/core/modules/migrate_drupal/tests/fixtures/drupal6.php
@@ -8034,7 +8034,17 @@
 ->values(array(
   'uid' => '1',
   'nid' => '9',
-  'timestamp' => '1457655127',
+  'timestamp' => '1468384961',
+))
+->values(array(
+  'uid' => '1',
+  'nid' => '12',
+  'timestamp' => '1468384823',
+))
+->values(array(
+  'uid' => '1',
+  'nid' => '13',
+  'timestamp' => '1468384931',
 ))
 ->execute();
 
@@ -34709,7 +34719,7 @@
   'access_callback' => 'user_access',
   'access_arguments' => 'a:1:{i:0;s:24:"administer content types";}',
   'page_callback' => 'drupal_get_form',
-  'page_arguments' => 'a:2:{i:0;s:14:"node_type_form";i:1;O:8:"stdClass":14:{s:4:"type";s:7:"company";s:4:"name";s:7:"Company";s:6:"module";s:4:"node";s:11:"description";s:17:"Company node type";s:4:"help";s:0:"";s:9:"has_title";s:1:"1";s:11:"title_label";s:4:"Name";s:8:"has_body";s:1:"1";s:10:"body_label";s:11:"Description";s:14:"min_word_count";s:2:"20";s:6:"custom";s:1:"0";s:8:"modified";s:1:"0";s:6:"locked";s:1:"0";s:9:"orig_type";s:7:"company";}}',
+  'page_arguments' => 'a:2:{i:0;s:14:"node_type_form";i:1;O:8:"stdClass":14:{s:4:"type";s:7:"company";s:4:"name";s:7:"Company";s:6:"module";s:4:"node";s:11:"description";s:17:"Company node type";s:4:"help";s:0:"";s:9:"has_title";s:1:"1";s:11:"title_label";s:4:"Name";s:8:"has_body";s:1:"1";s:10:"body_label";s:11:"Description";s:14:"min_word_count";s:1:"0";s:6:"custom";s:1:"0";s:8:"modified";s:1:"1";s:6:"locked";s:1:"0";s:9:"orig_type";s:7:"company";}}',
   'fit' => '15',
   'number_parts' => '4',
   'tab_parent' => '',
@@ -34731,7 +34741,7 @@
   'access_callback' => 'user_access',
   'access_arguments' => 'a:1:{i:0;s:24:"administer content types";}',
   'page_callback' => 'drupal_get_form',
-  'page_arguments' => 'a:2:{i:0;s:24:"node_type_delete_confirm";i:1;O:8:"stdClass":14:{s:4:"type";s:7:"company";s:4:"name";s:7:"Company";s:6:"module";s:4:"node";s:11:"description";s:17:"Company node type";s:4:"help";s:0:"";s:9:"has_title";s:1:"1";s:11:"title_label";s:4:"Name";s:8:"has_body";s:1:"1";s:10:"body_label";s:11:"Description";s:14:"min_word_count";s:2:"20";s:6:"custom";s:1:"0";s:8:"modified";s:1:"0";s:6:"locked";s:1:"0";s:9:"orig_type";s:7:"company";}}',
+  'page_arguments' => 'a:2:{i:0;s:24:"node_type_delete_confirm";i:1;O:8:"stdClass":14:{s:4:"type";s:7:"company";s:4:"name";s:7:"Company";s:6:"module";s:4:"node";s:11:"description";s:17:"Company node type";s:4:"help";s:0:"";s:9:"has_title";s:1:"1";s:11:"title_label";s:4:"Name";s:8:"has_body";s:1:"1";s:10:"body_label";s:11:"Description";s:14:"min_word_count";s:1:"0";s:6:"custom";s:1:"0";s:8:"modified";s:1:"1";s:6:"locked";s:1:"0";s:9:"orig_type";s:7:"company";}}',
   'fit' => '31',
   'number_parts' => '5',
   'tab_parent' => '',
@@ -34841,7 +34851,7 @@
   'access_callback' => 'user_access',
   'access_arguments' => 'a:1:{i:0;s:24:"administer content types";}',
   'page_callback' => 'drupal_get_form',
-  'page_arguments' => 'a:2:{i:0;s:14:"node_type_form";i:1;O:8:"stdClass":14:{s:4:"type";s:7:"company";s:4:"name";s:7:"Company";s:6:"module";s:4:"node";s:11:"description";s:17:"Company node type";s:4:"help";s:0:"";s:9:"has_title";s:1:"1";s:11:"title_label";s:4:"Name";s:8:"has_body";s:1:"1";s:10:"body_label";s:11:"Description";s:14:"min_word_count";s:2:"20";s:6:"custom";s:1:"0";s:8:"modified";s:1:"0";s:6:"locked";s:1:"0";s:9:"orig_type";s:7:"company";}}',
+  'page_arguments' => 'a:2:{i:0;s:14:"node_type_form";i:1;O:8:"stdClass":14:{s:4:"type";s:7:"company";s:4:"name";s:7:"Company";s:6:"module";s:4:"node";s:11:"description";s:17:"Company node type";s:4:"help";s:0:"";s:9:"has_title";s:1:"1";s:11:"title_label";s:4:"Name";s:8:"has_body";s:1:"1";s:10:"body_label";s:11:"Description";s:14:"min_word_count";s:1:"0";s:6:"custom";s:1:"0";s:8:"modified";s:1:"1";s:6:"locked";s:1:"0";s:9:"orig_type";s:7:"company";}}',
   'fit' => '31',
   'number_parts' => '5',
   'tab_parent' => 'admin/content/node-type/company',
@@ -41334,6 +41344,40 @@
   'tnid' => '0',
   'translate' => '0',
 ))
+->values(array(
+  'nid' => '10',
+  'vid' => '13',
+  'type' => 'page',
+  'language' => 'en',
+  'title' => 'The Real McCoy',
+  'uid' => '1',
+  'status' => '1',
+  'created' => '1444238800',
+  'changed' => '1444238808',
+  'comment' => '2',
+  'promote' => '1',
+  'moderate' => '0',
+  'sticky' => '0',
+  'tnid' => '10',
+  'translate' => '0',
+))
+->values(array(
+  'nid' => '11',
+  'vid' => '14',
+  'type' => 'page',
+  'language' => 'fr',
+  'title' => 'Le Vrai McCoy',
+  'uid' => '1',
+  'status' => '1',
+  'created' => '1444239050',
+  'changed' => '1444239050',
+  'comment' => '2',
+  'promote' => '1',
+  'moderate' => '0',
+  'sticky' => '0',
+  'tnid' => '10',
+  'translate' => '0',
+))
 ->execute();
 
 $connection->schema()->createTable('node_access', array(
@@ -41388,25 +41432,6 @@
   'mysql_character_set' => 'utf8',
 ));
 
-$connection->insert('node_access')
-->fields(array(
-  'nid',
-  'gid',
-  'realm',
-  'grant_view',
-  'grant_update',
-  'grant_delete',
-))
-->values(array(
-  'nid' => '0',
-  'gid' => '0',
-  'realm' => 'all',
-  'grant_view' => '1',
-  'grant_update' => '0',
-  'grant_delete' => '0',
-))
-->execute();
-
 $connection->schema()->createTable('node_comment_statistics', array(
   'fields' => array(
     'nid' => array(
@@ -41464,6 +41489,13 @@
   'comment_count',
 ))
 ->values(array(
+  'nid' => '0',
+  'last_comment_timestamp' => '1468384735',
+  'last_comment_name' => NULL,
+  'last_comment_uid' => '1',
+  'comment_count' => '0',
+))
+->values(array(
   'nid' => '1',
   'last_comment_timestamp' => '1388271197',
   'last_comment_name' => NULL,
@@ -41479,7 +41511,14 @@
 ))
 ->values(array(
   'nid' => '9',
-  'last_comment_timestamp' => '1444671588',
+  'last_comment_timestamp' => '1444238800',
+  'last_comment_name' => NULL,
+  'last_comment_uid' => '1',
+  'comment_count' => '0',
+))
+->values(array(
+  'nid' => '10',
+  'last_comment_timestamp' => '1444239050',
   'last_comment_name' => NULL,
   'last_comment_uid' => '1',
   'comment_count' => '0',
@@ -41727,6 +41766,28 @@
   'timestamp' => '1444671588',
   'format' => '1',
 ))
+->values(array(
+  'nid' => '10',
+  'vid' => '13',
+  'uid' => '1',
+  'title' => 'The Real McCoy',
+  'body' => "In the original, Queen's English.",
+  'teaser' => "In the original, Queen's English.",
+  'log' => '',
+  'timestamp' => '1444238808',
+  'format' => '1',
+))
+->values(array(
+  'nid' => '11',
+  'vid' => '14',
+  'uid' => '1',
+  'title' => 'Le Vrai McCoy',
+  'body' => 'Ooh là là!',
+  'teaser' => 'Ooh là là!',
+  'log' => '',
+  'timestamp' => '1444239050',
+  'format' => '1',
+))
 ->execute();
 
 $connection->schema()->createTable('node_type', array(
@@ -41861,9 +41922,9 @@
   'title_label' => 'Name',
   'has_body' => '1',
   'body_label' => 'Description',
-  'min_word_count' => '20',
+  'min_word_count' => '0',
   'custom' => '0',
-  'modified' => '0',
+  'modified' => '1',
   'locked' => '0',
   'orig_type' => 'company',
 ))
@@ -44465,8 +44526,8 @@
   'signature' => '',
   'signature_format' => '0',
   'created' => '0',
-  'access' => '1458198052',
-  'login' => '1458193160',
+  'access' => '1468384823',
+  'login' => '1468384420',
   'status' => '1',
   'timezone' => NULL,
   'language' => '',
@@ -44810,12 +44871,16 @@
   'value' => 's:1:"2";',
 ))
 ->values(array(
+  'name' => 'comment_company',
+  'value' => 's:1:"2";',
+))
+->values(array(
   'name' => 'comment_controls_article',
   'value' => 'i:3;',
 ))
 ->values(array(
   'name' => 'comment_controls_company',
-  'value' => 'i:3;',
+  'value' => 's:1:"3";',
 ))
 ->values(array(
   'name' => 'comment_controls_employee',
@@ -44855,7 +44920,7 @@
 ))
 ->values(array(
   'name' => 'comment_default_mode_company',
-  'value' => 'i:4;',
+  'value' => 's:1:"4";',
 ))
 ->values(array(
   'name' => 'comment_default_mode_employee',
@@ -44895,7 +44960,7 @@
 ))
 ->values(array(
   'name' => 'comment_default_order_company',
-  'value' => 'i:1;',
+  'value' => 's:1:"1";',
 ))
 ->values(array(
   'name' => 'comment_default_order_employee',
@@ -44935,7 +45000,7 @@
 ))
 ->values(array(
   'name' => 'comment_default_per_page_company',
-  'value' => 'i:50;',
+  'value' => 's:2:"50";',
 ))
 ->values(array(
   'name' => 'comment_default_per_page_employee',
@@ -44975,7 +45040,7 @@
 ))
 ->values(array(
   'name' => 'comment_form_location_company',
-  'value' => 'i:0;',
+  'value' => 's:1:"0";',
 ))
 ->values(array(
   'name' => 'comment_form_location_employee',
@@ -45019,7 +45084,7 @@
 ))
 ->values(array(
   'name' => 'comment_preview_company',
-  'value' => 'i:1;',
+  'value' => 's:1:"1";',
 ))
 ->values(array(
   'name' => 'comment_preview_employee',
@@ -45063,7 +45128,7 @@
 ))
 ->values(array(
   'name' => 'comment_subject_field_company',
-  'value' => 'i:1;',
+  'value' => 's:1:"1";',
 ))
 ->values(array(
   'name' => 'comment_subject_field_employee',
@@ -45398,6 +45463,18 @@
   'value' => 's:22:"\S\H\O\R\T m/d/Y - H:i";',
 ))
 ->values(array(
+  'name' => 'date_max_year',
+  'value' => 'i:4000;',
+))
+->values(array(
+  'name' => 'date_min_year',
+  'value' => 'i:1;',
+))
+->values(array(
+  'name' => 'date_php_min_year',
+  'value' => 'i:1901;',
+))
+->values(array(
   'name' => 'dblog_row_limit',
   'value' => 'i:10000;',
 ))
@@ -45426,6 +45503,10 @@
   'value' => 's:5:"never";',
 ))
 ->values(array(
+  'name' => 'event_nodeapi_company',
+  'value' => 's:5:"never";',
+))
+->values(array(
   'name' => 'event_nodeapi_event',
   'value' => 's:3:"all";',
 ))
@@ -45487,7 +45568,11 @@
 ))
 ->values(array(
   'name' => 'form_build_id_article',
-  'value' => 's:48:"form-mXZfFJxcCFGB80PPYtNOuwYbho6-xKTvrRLb3TAMkic";',
+  'value' => 's:48:"form-t2zKJflpBD4rpYoGQH33ckjjWAYdo5lF3Hl1O_YnWyE";',
+))
+->values(array(
+  'name' => 'form_build_id_company',
+  'value' => 's:48:"form-jFw2agRukPxjG5dG-N6joZLyoxXmCoxTzua0HUciqK0";',
 ))
 ->values(array(
   'name' => 'forum_block_num_0',
@@ -45518,6 +45603,14 @@
   'value' => 'a:2:{i:0;i:1;i:1;i:2;}',
 ))
 ->values(array(
+  'name' => 'i18ntaxonomy_vocabulary',
+  'value' => 'a:1:{i:4;s:1:"0";}',
+))
+->values(array(
+  'name' => 'i18n_lock_node_article',
+  'value' => 'i:1;',
+))
+->values(array(
   'name' => 'image_jpeg_quality',
   'value' => 'i:75;',
 ))
@@ -45527,7 +45620,7 @@
 ))
 ->values(array(
   'name' => 'javascript_parsed',
-  'value' => 'a:21:{i:0;s:14:"misc/jquery.js";i:1;s:14:"misc/drupal.js";i:2;s:19:"misc/tableheader.js";i:3;s:16:"misc/collapse.js";i:4;s:16:"misc/textarea.js";i:5;s:20:"modules/user/user.js";i:6;s:17:"misc/tabledrag.js";i:7;s:26:"modules/profile/profile.js";i:8;s:12:"misc/form.js";i:9;s:19:"misc/tableselect.js";i:10;s:20:"misc/autocomplete.js";s:10:"refresh:ga";s:7:"waiting";s:10:"refresh:ab";s:7:"waiting";s:10:"refresh:ca";s:7:"waiting";s:10:"refresh:fi";s:7:"waiting";s:10:"refresh:es";s:7:"waiting";i:11;s:16:"misc/progress.js";i:12;s:13:"misc/batch.js";s:10:"refresh:nl";s:7:"waiting";s:10:"refresh:de";s:7:"waiting";s:10:"refresh:pl";s:7:"waiting";}',
+  'value' => 'a:28:{i:0;s:14:"misc/jquery.js";i:1;s:14:"misc/drupal.js";i:2;s:19:"misc/tableheader.js";i:3;s:16:"misc/collapse.js";i:4;s:16:"misc/textarea.js";i:5;s:20:"modules/user/user.js";i:6;s:17:"misc/tabledrag.js";i:7;s:26:"modules/profile/profile.js";i:8;s:12:"misc/form.js";i:9;s:19:"misc/tableselect.js";i:10;s:20:"misc/autocomplete.js";s:10:"refresh:ga";s:7:"waiting";s:10:"refresh:ab";s:7:"waiting";s:10:"refresh:ca";s:7:"waiting";s:10:"refresh:fi";s:7:"waiting";s:10:"refresh:es";s:7:"waiting";i:11;s:16:"misc/progress.js";i:12;s:13:"misc/batch.js";s:10:"refresh:nl";s:7:"waiting";s:10:"refresh:de";s:7:"waiting";s:10:"refresh:pl";s:7:"waiting";i:13;s:32:"sites/all/modules/cck/content.js";s:10:"refresh:fr";s:7:"waiting";s:10:"refresh:zu";s:7:"waiting";i:14;s:19:"misc/jquery.form.js";i:15;s:12:"misc/ahah.js";i:16;s:14:"misc/teaser.js";i:17;s:51:"sites/all/modules/i18n/i18ntaxonomy/i18ntaxonomy.js";}',
 ))
 ->values(array(
   'name' => 'language_content_type_article',
@@ -45538,10 +45631,6 @@
   'value' => 's:1:"2";',
 ))
 ->values(array(
-  'name' => 'i18n_lock_node_article',
-  'value' => 'i:1;',
-))
-->values(array(
   'name' => 'language_count',
   'value' => 'i:11;',
 ))
@@ -45590,6 +45679,10 @@
   'value' => 'a:1:{i:0;s:6:"status";}',
 ))
 ->values(array(
+  'name' => 'node_options_company',
+  'value' => 'a:2:{i:0;s:6:"status";i:1;s:7:"promote";}',
+))
+->values(array(
   'name' => 'node_options_forum',
   'value' => 'a:1:{i:0;s:6:"status";}',
 ))
@@ -45786,6 +45879,10 @@
   'value' => 'b:0;',
 ))
 ->values(array(
+  'name' => 'upload_company',
+  'value' => 's:1:"1";',
+))
+->values(array(
   'name' => 'upload_page',
   'value' => 'b:1;',
 ))
@@ -46033,7 +46130,7 @@
   'relations' => '1',
   'hierarchy' => '0',
   'multiple' => '0',
-  'required' => '1',
+  'required' => '0',
   'tags' => '0',
   'module' => 'taxonomy',
   'weight' => '0',
@@ -46088,10 +46185,6 @@
   'type' => 'article',
 ))
 ->values(array(
-  'vid' => '4',
-  'type' => 'page',
-))
-->values(array(
   'vid' => '1',
   'type' => 'story',
 ))
diff --git a/core/modules/migrate_drupal/tests/src/Kernel/d6/EntityContentBaseTest.php b/core/modules/migrate_drupal/tests/src/Kernel/d6/EntityContentBaseTest.php
index bdac5c9..5074473 100644
--- a/core/modules/migrate_drupal/tests/src/Kernel/d6/EntityContentBaseTest.php
+++ b/core/modules/migrate_drupal/tests/src/Kernel/d6/EntityContentBaseTest.php
@@ -4,7 +4,10 @@
 
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\migrate\MigrateExecutable;
+use Drupal\migrate\MigrateMessageInterface;
 use Drupal\user\Entity\User;
+use Prophecy\Argument;
 
 /**
  * @group migrate_drupal
@@ -85,4 +88,40 @@ public function testOverwriteProperties() {
     $this->assertIdentical('proto@zo.an', $account->getInitialEmail());
   }
 
+  /**
+   * Test that translation destination fails for untranslatable entities.
+   */
+  public function testUntranslatable() {
+    $this->enableModules(['language_test']);
+    $this->installEntitySchema('no_language_entity_test');
+
+    /** @var MigrationInterface $migration */
+    $migration = \Drupal::service('plugin.manager.migration')->createStubMigration([
+      'source' => [
+        'plugin' => 'embedded_data',
+        'ids' => ['id' => ['type' => 'integer']],
+        'data_rows' => [['id' => 1]],
+      ],
+      'process' => [
+        'id' => 'id',
+      ],
+      'destination' => [
+        'plugin' => 'entity:no_language_entity_test',
+        'translations' => TRUE,
+      ],
+    ]);
+
+    $message = $this->prophesize(MigrateMessageInterface::class);
+    // Match the expected message. Can't use default argument types, because
+    // we need to convert to string from TranslatableMarkup.
+    $argument = Argument::that(function($msg) {
+      return strpos((string) $msg, "This entity type does not support translation") !== FALSE;
+    });
+    $message->display($argument, Argument::any())
+      ->shouldBeCalled();
+
+    $executable = new MigrateExecutable($migration, $message->reveal());
+    $executable->import();
+  }
+
 }
diff --git a/core/modules/migrate_drupal/tests/src/Kernel/d6/MigrateDrupal6TestBase.php b/core/modules/migrate_drupal/tests/src/Kernel/d6/MigrateDrupal6TestBase.php
index 72a8095..3a9467e 100644
--- a/core/modules/migrate_drupal/tests/src/Kernel/d6/MigrateDrupal6TestBase.php
+++ b/core/modules/migrate_drupal/tests/src/Kernel/d6/MigrateDrupal6TestBase.php
@@ -89,17 +89,24 @@ protected function migrateFields() {
   /**
    * Executes all content migrations.
    *
-   * @param bool $include_revisions
-   *   If TRUE, migrates node revisions.
+   * @param array $include
+   *   Extra things to include as part of the migrations. Values may be
+   *   'revisions' or 'translations'.
    */
-  protected function migrateContent($include_revisions = FALSE) {
+  protected function migrateContent($include = []) {
+    if (in_array('translations', $include)) {
+      $this->executeMigrations(['language']);
+    }
     $this->migrateUsers(FALSE);
     $this->migrateFields();
 
     $this->installEntitySchema('node');
     $this->executeMigrations(['d6_node_settings', 'd6_node']);
 
-    if ($include_revisions) {
+    if (in_array('translations', $include)) {
+      $this->executeMigrations(['translations']);
+    }
+    if (in_array('revisions', $include)) {
       $this->executeMigrations(['d6_node_revision']);
     }
   }
diff --git a/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php
index 5808c1f..b437852 100644
--- a/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php
+++ b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php
@@ -266,6 +266,14 @@ class MigrateUpgradeForm extends ConfirmFormBase {
       'source_module' => 'image',
       'destination_module' => 'image',
     ],
+    'd6_language_content_settings' => [
+      'source_module' => 'locale',
+      'destination_module' => 'language',
+    ],
+    'd7_language_content_settings' => [
+      'source_module' => 'locale',
+      'destination_module' => 'language',
+    ],
     'd7_language_negotiation_settings' => [
       'source_module' => 'locale',
       'destination_module' => 'language',
@@ -290,6 +298,10 @@ class MigrateUpgradeForm extends ConfirmFormBase {
       'source_module' => 'node',
       'destination_module' => 'node',
     ],
+    'd6_node_translation' => [
+      'source_module' => 'node',
+      'destination_module' => 'node',
+    ],
     'd6_node_revision' => [
       'source_module' => 'node',
       'destination_module' => 'node',
diff --git a/core/modules/migrate_drupal_ui/src/Tests/MigrateUpgradeTestBase.php b/core/modules/migrate_drupal_ui/src/Tests/MigrateUpgradeTestBase.php
index 8cf8c8d..c139291 100644
--- a/core/modules/migrate_drupal_ui/src/Tests/MigrateUpgradeTestBase.php
+++ b/core/modules/migrate_drupal_ui/src/Tests/MigrateUpgradeTestBase.php
@@ -30,7 +30,7 @@
    *
    * @var array
    */
-  public static $modules = ['migrate_drupal_ui', 'telephone'];
+  public static $modules = ['language', 'content_translation', 'migrate_drupal_ui', 'telephone'];
 
   /**
    * {@inheritdoc}
diff --git a/core/modules/migrate_drupal_ui/src/Tests/d6/MigrateUpgrade6Test.php b/core/modules/migrate_drupal_ui/src/Tests/d6/MigrateUpgrade6Test.php
index ea0b588..fe31771 100644
--- a/core/modules/migrate_drupal_ui/src/Tests/d6/MigrateUpgrade6Test.php
+++ b/core/modules/migrate_drupal_ui/src/Tests/d6/MigrateUpgrade6Test.php
@@ -40,14 +40,16 @@ protected function getEntityCounts() {
       'comment' => 3,
       'comment_type' => 2,
       'contact_form' => 5,
+      'configurable_language' => 5,
       'editor' => 2,
       'field_config' => 62,
       'field_storage_config' => 43,
       'file' => 7,
       'filter_format' => 7,
       'image_style' => 5,
+      'language_content_settings' => 2,
       'migration' => 105,
-      'node' => 9,
+      'node' => 10,
       'node_type' => 11,
       'rdf_mapping' => 5,
       'search_page' => 2,
@@ -57,7 +59,7 @@ protected function getEntityCounts() {
       'menu' => 8,
       'taxonomy_term' => 6,
       'taxonomy_vocabulary' => 6,
-      'tour' => 1,
+      'tour' => 4,
       'user' => 7,
       'user_role' => 6,
       'menu_link_content' => 4,
diff --git a/core/modules/migrate_drupal_ui/src/Tests/d7/MigrateUpgrade7Test.php b/core/modules/migrate_drupal_ui/src/Tests/d7/MigrateUpgrade7Test.php
index 1d3ce61..be807e4 100644
--- a/core/modules/migrate_drupal_ui/src/Tests/d7/MigrateUpgrade7Test.php
+++ b/core/modules/migrate_drupal_ui/src/Tests/d7/MigrateUpgrade7Test.php
@@ -39,6 +39,8 @@ protected function getEntityCounts() {
       'block_content_type' => 1,
       'comment' => 1,
       'comment_type' => 7,
+      // Module 'language' comes with 'en', 'und', 'zxx'. Migration adds 'is'.
+      'configurable_language' => 4,
       'contact_form' => 3,
       'editor' => 2,
       'field_config' => 41,
@@ -46,6 +48,7 @@ protected function getEntityCounts() {
       'file' => 1,
       'filter_format' => 7,
       'image_style' => 6,
+      'language_content_settings' => 1,
       'migration' => 59,
       'node' => 2,
       'node_type' => 6,
@@ -57,7 +60,7 @@ protected function getEntityCounts() {
       'menu' => 10,
       'taxonomy_term' => 18,
       'taxonomy_vocabulary' => 3,
-      'tour' => 1,
+      'tour' => 4,
       'user' => 3,
       'user_role' => 4,
       'menu_link_content' => 9,
diff --git a/core/modules/node/migration_templates/d6_node.yml b/core/modules/node/migration_templates/d6_node.yml
index ec1474a..58aea35 100644
--- a/core/modules/node/migration_templates/d6_node.yml
+++ b/core/modules/node/migration_templates/d6_node.yml
@@ -6,7 +6,10 @@ deriver: Drupal\node\Plugin\migrate\D6NodeDeriver
 source:
   plugin: d6_node
 process:
-  nid: nid
+  # In D6, nodes always have a tnid, but it's zero for untranslated nodes.
+  # We normalize it to equal the nid in that case.
+  # @see \Drupal\node\Plugin\migrate\source\d6\Node::prepareRow().
+  nid: tnid
   vid: vid
   langcode:
     plugin: default_value
diff --git a/core/modules/node/migration_templates/d6_node_translation.yml b/core/modules/node/migration_templates/d6_node_translation.yml
new file mode 100644
index 0000000..2ddd5a4
--- /dev/null
+++ b/core/modules/node/migration_templates/d6_node_translation.yml
@@ -0,0 +1,51 @@
+id: d6_node_translation
+label: Node translations
+migration_tags:
+  - Drupal 6
+deriver: Drupal\node\Plugin\migrate\D6NodeDeriver
+source:
+  plugin: d6_node
+  translations: true
+process:
+  nid: tnid
+  type: type
+  langcode:
+    plugin: default_value
+    source: language
+    default_value: "und"
+  title: title
+  uid: node_uid
+  status: status
+  created: created
+  changed: changed
+  promote: promote
+  sticky: sticky
+  'body/format':
+    plugin: migration
+    migration: d6_filter_format
+    source: format
+  'body/value': body
+  'body/summary': teaser
+  revision_uid: revision_uid
+  revision_log: log
+  revision_timestamp: timestamp
+
+#  unmapped d6 fields.
+#  translate
+#  moderate
+#  comment
+
+destination:
+  plugin: entity:node
+  translations: true
+migration_dependencies:
+  required:
+    - d6_user
+    - d6_node_type
+    - d6_node_settings
+    - d6_filter_format
+    - language
+  optional:
+    - d6_field_instance_widget_settings
+    - d6_field_formatter_settings
+    - d6_upload_field_instance
diff --git a/core/modules/node/src/Plugin/migrate/D6NodeDeriver.php b/core/modules/node/src/Plugin/migrate/D6NodeDeriver.php
index c8a1397..f3e1a92 100644
--- a/core/modules/node/src/Plugin/migrate/D6NodeDeriver.php
+++ b/core/modules/node/src/Plugin/migrate/D6NodeDeriver.php
@@ -38,25 +38,37 @@ class D6NodeDeriver extends DeriverBase implements ContainerDeriverInterface {
   protected $cckPluginManager;
 
   /**
+   * Whether or not to include translations.
+   *
+   * @var bool
+   */
+  protected $includeTranslations;
+
+  /**
    * D6NodeDeriver constructor.
    *
    * @param string $base_plugin_id
    *   The base plugin ID for the plugin ID.
    * @param \Drupal\Component\Plugin\PluginManagerInterface $cck_manager
    *   The CCK plugin manager.
+   * @param bool $translations
+   *   Whether or not to include translations.
    */
-  public function __construct($base_plugin_id, PluginManagerInterface $cck_manager) {
+  public function __construct($base_plugin_id, PluginManagerInterface $cck_manager, $translations) {
     $this->basePluginId = $base_plugin_id;
     $this->cckPluginManager = $cck_manager;
+    $this->includeTranslations = $translations;
   }
 
   /**
    * {@inheritdoc}
    */
   public static function create(ContainerInterface $container, $base_plugin_id) {
+    // Translations don't make sense unless we have content_translation.
     return new static(
       $base_plugin_id,
-      $container->get('plugin.manager.migrate.cckfield')
+      $container->get('plugin.manager.migrate.cckfield'),
+      $container->get('module_handler')->moduleExists('content_translation')
     );
   }
 
@@ -72,6 +84,11 @@ public static function create(ContainerInterface $container, $base_plugin_id) {
    * @see \Drupal\Component\Plugin\Derivative\DeriverBase::getDerivativeDefinition()
    */
   public function getDerivativeDefinitions($base_plugin_definition) {
+    if ($base_plugin_definition['id'] == 'd6_node_translation' && !$this->includeTranslations) {
+      // Refuse to generate anything.
+      return $this->derivatives;
+    }
+
     // Read all CCK field instance definitions in the source database.
     $fields = array();
     try {
@@ -100,9 +117,10 @@ public function getDerivativeDefinitions($base_plugin_definition) {
         $values['source']['node_type'] = $node_type;
         $values['destination']['default_bundle'] = $node_type;
 
-        // If this migration is based on the d6_node_revision migration, it
-        // should explicitly depend on the corresponding d6_node variant.
-        if ($base_plugin_definition['id'] == 'd6_node_revision') {
+        // If this migration is based on the d6_node_revision migration or
+        // is for translations of nodes, it should explicitly depend on the
+        // corresponding d6_node variant.
+        if (in_array($base_plugin_definition['id'], ['d6_node_revision', 'd6_node_translation'])) {
           $values['migration_dependencies']['required'][] = 'd6_node:' . $node_type;
         }
 
diff --git a/core/modules/node/src/Plugin/migrate/source/d6/Node.php b/core/modules/node/src/Plugin/migrate/source/d6/Node.php
index 1ed96f9..5ca698f 100644
--- a/core/modules/node/src/Plugin/migrate/source/d6/Node.php
+++ b/core/modules/node/src/Plugin/migrate/source/d6/Node.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\node\Plugin\migrate\source\d6;
 
+use Drupal\Core\Database\Query\SelectInterface;
 use Drupal\migrate\Row;
 use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
 
@@ -37,9 +38,11 @@ class Node extends DrupalSqlBase {
    * {@inheritdoc}
    */
   public function query() {
-    // Select node in its last revision.
-    $query = $this->select('node_revisions', 'nr')
-      ->fields('n', array(
+    $query = $this->select('node_revisions', 'nr');
+    $query->innerJoin('node', 'n', static::JOIN);
+    $this->handleTranslations($query);
+
+    $query->fields('n', array(
         'nid',
         'type',
         'language',
@@ -54,17 +57,16 @@ public function query() {
         'translate',
       ))
       ->fields('nr', array(
-        'vid',
         'title',
         'body',
         'teaser',
         'log',
         'timestamp',
         'format',
+        'vid',
       ));
     $query->addField('n', 'uid', 'node_uid');
     $query->addField('nr', 'uid', 'revision_uid');
-    $query->innerJoin('node', 'n', static::JOIN);
 
     if (isset($this->configuration['node_type'])) {
       $query->condition('n.type', $this->configuration['node_type']);
@@ -123,6 +125,11 @@ public function prepareRow(Row $row) {
       }
     }
 
+    // Make sure we always have a translation set.
+    if ($row->getSourceProperty('tnid') == 0) {
+      $row->setSourceProperty('tnid', $row->getSourceProperty('nid'));
+    }
+
     return parent::prepareRow($row);
   }
 
@@ -251,4 +258,22 @@ public function getIds() {
     return $ids;
   }
 
+  /**
+   * Adapt our query for translations.
+   *
+   * @param \Drupal\Core\Database\Query\SelectInterface
+   *   The generated query.
+   */
+  protected function handleTranslations(SelectInterface $query) {
+    // Check whether or not we want translations.
+    if (empty($this->configuration['translations'])) {
+      // No translations: Yield untranslated nodes, or default translations.
+      $query->where('n.tnid = 0 OR n.tnid = n.nid');
+    }
+    else {
+      // Translations: Yield only non-default translations.
+      $query->where('n.tnid <> 0 AND n.tnid <> n.nid');
+    }
+  }
+
 }
diff --git a/core/modules/node/src/Plugin/migrate/source/d6/NodeRevision.php b/core/modules/node/src/Plugin/migrate/source/d6/NodeRevision.php
index 41e7a18..3f9a580 100644
--- a/core/modules/node/src/Plugin/migrate/source/d6/NodeRevision.php
+++ b/core/modules/node/src/Plugin/migrate/source/d6/NodeRevision.php
@@ -1,6 +1,7 @@
 <?php
 
 namespace Drupal\node\Plugin\migrate\source\d6;
+use Drupal\Core\Database\Query\SelectInterface;
 
 /**
  * Drupal 6 node revision source from database.
@@ -37,4 +38,11 @@ public function getIds() {
     return $ids;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function handleTranslations(SelectInterface $query) {
+    // @todo in https://www.drupal.org/node/2746541
+  }
+
 }
diff --git a/core/modules/node/src/Tests/Migrate/d6/MigrateNodeRevisionTest.php b/core/modules/node/src/Tests/Migrate/d6/MigrateNodeRevisionTest.php
index 05be99f..a02ef3f 100644
--- a/core/modules/node/src/Tests/Migrate/d6/MigrateNodeRevisionTest.php
+++ b/core/modules/node/src/Tests/Migrate/d6/MigrateNodeRevisionTest.php
@@ -13,6 +13,11 @@ class MigrateNodeRevisionTest extends MigrateNodeTestBase {
   /**
    * {@inheritdoc}
    */
+  public static $modules = ['language', 'content_translation'];
+
+  /**
+   * {@inheritdoc}
+   */
   protected function setUp() {
     parent::setUp();
     $this->executeMigrations(['d6_node', 'd6_node_revision']);
diff --git a/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeDeriverTest.php b/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeDeriverTest.php
new file mode 100644
index 0000000..abce689
--- /dev/null
+++ b/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeDeriverTest.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\Tests\node\Kernel\Migrate\d6;
+
+use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
+
+/**
+ * Test D6NodeDeriver.
+ *
+ * @group migrate_drupal_6
+ */
+class MigrateNodeDeriverTest extends MigrateDrupal6TestBase {
+  /**
+   * The migration plugin manager.
+   *
+   * @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
+   */
+  protected $pluginManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+    $this->pluginManager = $this->container->get('plugin.manager.migration');
+  }
+
+  /**
+   * Test node translation migrations with translation disabled.
+   */
+  public function testNoTranslations() {
+    // Without content_translation, there should be no translation migrations.
+    $migrations = $this->pluginManager->createInstances('d6_node_translation');
+    $this->assertSame([], $migrations,
+      "No node translation migrations without content_translation");
+  }
+
+  /**
+   * Test node translation migrations with translation enabled.
+   */
+  public function testTranslations() {
+    // With content_translation, there should be translation migrations for
+    // each content type.
+    $this->enableModules(['language', 'content_translation']);
+    $migrations = $this->pluginManager->createInstances('d6_node_translation');
+    $this->assertArrayHasKey('d6_node_translation:story', $migrations,
+      "Node translation migrations exist after content_translation installed");
+  }
+
+}
diff --git a/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeTest.php b/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeTest.php
index 5f48683..122199f 100644
--- a/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeTest.php
+++ b/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeTest.php
@@ -19,11 +19,16 @@ class MigrateNodeTest extends MigrateNodeTestBase {
   /**
    * {@inheritdoc}
    */
+  public static $modules = ['language', 'content_translation'];
+
+  /**
+   * {@inheritdoc}
+   */
   protected function setUp() {
     parent::setUp();
     $this->setUpMigratedFiles();
     $this->installSchema('file', ['file_usage']);
-    $this->executeMigrations(['d6_node']);
+    $this->executeMigrations(['language', 'd6_node', 'd6_node_translation']);
   }
 
   /**
@@ -85,6 +90,15 @@ public function testNode() {
     $this->assertSame('Buy it now', $node->field_test_link->title);
     $this->assertSame(['attributes' => ['target' => '_blank']], $node->field_test_link->options);
 
+    // Test that translations are working.
+    $node = Node::load(10);
+    $this->assertIdentical('en', $node->langcode->value);
+    $this->assertIdentical('The Real McCoy', $node->title->value);
+    $this->assertTrue($node->hasTranslation('fr'), "Node 10 has french translation");
+
+    // Node 11 is a translation of node 10, and should not be imported separately.
+    $this->assertNull(Node::load(11), "Node 11 doesn't exist in D8, it was a translation");
+
     // Rerun migration with two source database changes.
     // 1. Add an invalid link attributes and a different URL and
     // title. If only the attributes are changed the error does not occur.
diff --git a/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeByNodeTypeTest.php b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeByNodeTypeTest.php
index 0c31810..6a308ff 100644
--- a/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeByNodeTypeTest.php
+++ b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeByNodeTypeTest.php
@@ -40,7 +40,7 @@ class NodeByNodeTypeTest extends MigrateSqlSourceTestCase {
       'promote' => 1,
       'moderate' => 0,
       'sticky' => 0,
-      'tnid' => 0,
+      'tnid' => 1,
       'translate' => 0,
       // Node revision fields.
       'body' => 'body for node 1',
@@ -64,7 +64,7 @@ class NodeByNodeTypeTest extends MigrateSqlSourceTestCase {
       'promote' => 1,
       'moderate' => 0,
       'sticky' => 0,
-      'tnid' => 0,
+      'tnid' => 2,
       'translate' => 0,
       // Node revision fields.
       'body' => 'body for node 2',
diff --git a/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeRevisionByNodeTypeTest.php b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeRevisionByNodeTypeTest.php
index 8c78be7..d548ff3 100644
--- a/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeRevisionByNodeTypeTest.php
+++ b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeRevisionByNodeTypeTest.php
@@ -134,7 +134,7 @@ class NodeRevisionByNodeTypeTest extends MigrateSqlSourceTestCase {
       'promote' => 1,
       'moderate' => 0,
       'sticky' => 0,
-      'tnid' => 0,
+      'tnid' => 1,
       'translate' => 0,
       'vid' => 1,
       'node_uid' => 1,
@@ -156,7 +156,7 @@ class NodeRevisionByNodeTypeTest extends MigrateSqlSourceTestCase {
       'promote' => 1,
       'moderate' => 0,
       'sticky' => 0,
-      'tnid' => 0,
+      'tnid' => 1,
       'translate' => 0,
       'vid' => 3,
       'node_uid' => 1,
diff --git a/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeRevisionTest.php b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeRevisionTest.php
index b1c23f8..752ed9b 100644
--- a/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeRevisionTest.php
+++ b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeRevisionTest.php
@@ -133,7 +133,7 @@ class NodeRevisionTest extends MigrateSqlSourceTestCase {
       'promote' => 1,
       'moderate' => 0,
       'sticky' => 0,
-      'tnid' => 0,
+      'tnid' => 1,
       'translate' => 0,
       // Node revision fields.
       'vid' => 1,
@@ -157,7 +157,7 @@ class NodeRevisionTest extends MigrateSqlSourceTestCase {
       'promote' => 1,
       'moderate' => 0,
       'sticky' => 0,
-      'tnid' => 0,
+      'tnid' => 1,
       'translate' => 0,
       // Node revision fields.
       'vid' => 3,
diff --git a/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeTest.php b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeTest.php
index ce55d52..b6ce22e 100644
--- a/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeTest.php
+++ b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeTest.php
@@ -2,16 +2,12 @@
 
 namespace Drupal\Tests\node\Unit\Plugin\migrate\source\d6;
 
-use Drupal\Tests\migrate\Unit\MigrateSqlSourceTestCase;
-
 /**
  * Tests D6 node source plugin.
  *
  * @group node
  */
-class NodeTest extends MigrateSqlSourceTestCase {
-
-  const PLUGIN_CLASS = 'Drupal\node\Plugin\migrate\source\d6\Node';
+class NodeTest extends NodeTestBase {
 
   protected $migrationConfiguration = array(
     'id' => 'test',
@@ -36,7 +32,7 @@ class NodeTest extends MigrateSqlSourceTestCase {
       'promote' => 1,
       'moderate' => 0,
       'sticky' => 0,
-      'tnid' => 0,
+      'tnid' => 1,
       'translate' => 0,
       // Node revision fields.
       'body' => 'body for node 1',
@@ -60,7 +56,7 @@ class NodeTest extends MigrateSqlSourceTestCase {
       'promote' => 1,
       'moderate' => 0,
       'sticky' => 0,
-      'tnid' => 0,
+      'tnid' => 2,
       'translate' => 0,
       // Node revision fields.
       'body' => 'body for node 2',
@@ -83,7 +79,7 @@ class NodeTest extends MigrateSqlSourceTestCase {
       'promote' => 1,
       'moderate' => 0,
       'sticky' => 0,
-      'tnid' => 0,
+      'tnid' => 5,
       'translate' => 0,
       // Node revision fields.
       'body' => 'body for node 5',
@@ -98,79 +94,29 @@ class NodeTest extends MigrateSqlSourceTestCase {
         ),
       ),
     ),
+    array(
+      'nid' => 6,
+      'vid' => 6,
+      'type' => 'story',
+      'language' => 'en',
+      'title' => 'node title 6',
+      'uid' => 1,
+      'status' => 1,
+      'created' => 1279290909,
+      'changed' => 1279308994,
+      'comment' => 0,
+      'promote' => 1,
+      'moderate' => 0,
+      'sticky' => 0,
+      'tnid' => 6,
+      'translate' => 0,
+      // Node revision fields.
+      'body' => 'body for node 6',
+      'teaser' => 'body for node 6',
+      'log' => '',
+      'timestamp' => 1279308994,
+      'format' => 1,
+    ),
   );
 
-  /**
-   * {@inheritdoc}
-   */
-  protected function setUp() {
-    $this->databaseContents['content_node_field'] = array(
-      array(
-        'field_name' => 'field_test_four',
-        'type' => 'number_float',
-        'global_settings' => 'a:0:{}',
-        'required' => '0',
-        'multiple' => '0',
-        'db_storage' => '1',
-        'module' => 'number',
-        'db_columns' => 'a:1:{s:5:"value";a:3:{s:4:"type";s:5:"float";s:8:"not null";b:0;s:8:"sortable";b:1;}}',
-        'active' => '1',
-        'locked' => '0',
-      ),
-    );
-    $this->databaseContents['content_node_field_instance'] = array(
-      array(
-        'field_name' => 'field_test_four',
-        'type_name' => 'story',
-        'weight' => '3',
-        'label' => 'Float Field',
-        'widget_type' => 'number',
-        'widget_settings' => 'a:0:{}',
-        'display_settings' => 'a:0:{}',
-        'description' => 'An example float field.',
-        'widget_module' => 'number',
-        'widget_active' => '1',
-      ),
-    );
-    $this->databaseContents['content_type_story'] = array(
-      array(
-        'nid' => 5,
-        'vid' => 5,
-        'uid' => 5,
-        'field_test_four_value' => '3.14159',
-      ),
-    );
-    $this->databaseContents['system'] = array(
-      array(
-        'type' => 'module',
-        'name' => 'content',
-        'schema_version' => 6001,
-        'status' => TRUE,
-      ),
-    );
-    foreach ($this->expectedResults as $k => $row) {
-      foreach (array('nid', 'vid', 'title', 'uid', 'body', 'teaser', 'format', 'timestamp', 'log') as $field) {
-        $this->databaseContents['node_revisions'][$k][$field] = $row[$field];
-        switch ($field) {
-          case 'nid': case 'vid':
-            break;
-          case 'uid':
-            $this->databaseContents['node_revisions'][$k]['uid']++;
-            break;
-          default:
-            unset($row[$field]);
-            break;
-        }
-      }
-      $this->databaseContents['node'][$k] = $row;
-    }
-    array_walk($this->expectedResults, function (&$row) {
-      $row['node_uid'] = $row['uid'];
-      $row['revision_uid'] = $row['uid'] + 1;
-      unset($row['uid']);
-    });
-
-    parent::setUp();
-  }
-
 }
diff --git a/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeTestBase.php b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeTestBase.php
new file mode 100644
index 0000000..8850fc4
--- /dev/null
+++ b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeTestBase.php
@@ -0,0 +1,179 @@
+<?php
+
+namespace Drupal\Tests\node\Unit\Plugin\migrate\source\d6;
+
+use Drupal\Tests\migrate\Unit\MigrateSqlSourceTestCase;
+
+/**
+ * Base for D6 node migration tests.
+ */
+abstract class NodeTestBase extends MigrateSqlSourceTestCase {
+
+  const PLUGIN_CLASS = 'Drupal\node\Plugin\migrate\source\d6\Node';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    $this->databaseContents['content_node_field'] = array(
+      array(
+        'field_name' => 'field_test_four',
+        'type' => 'number_float',
+        'global_settings' => 'a:0:{}',
+        'required' => '0',
+        'multiple' => '0',
+        'db_storage' => '1',
+        'module' => 'number',
+        'db_columns' => 'a:1:{s:5:"value";a:3:{s:4:"type";s:5:"float";s:8:"not null";b:0;s:8:"sortable";b:1;}}',
+        'active' => '1',
+        'locked' => '0',
+      ),
+    );
+    $this->databaseContents['content_node_field_instance'] = array(
+      array(
+        'field_name' => 'field_test_four',
+        'type_name' => 'story',
+        'weight' => '3',
+        'label' => 'Float Field',
+        'widget_type' => 'number',
+        'widget_settings' => 'a:0:{}',
+        'display_settings' => 'a:0:{}',
+        'description' => 'An example float field.',
+        'widget_module' => 'number',
+        'widget_active' => '1',
+      ),
+    );
+    $this->databaseContents['content_type_story'] = array(
+      array(
+        'nid' => 5,
+        'vid' => 5,
+        'uid' => 5,
+        'field_test_four_value' => '3.14159',
+      ),
+    );
+    $this->databaseContents['system'] = array(
+      array(
+        'type' => 'module',
+        'name' => 'content',
+        'schema_version' => 6001,
+        'status' => TRUE,
+      ),
+    );
+    $this->databaseContents['node'] = [
+      [
+        'nid' => 1,
+        'vid' => 1,
+        'type' => 'page',
+        'language' => 'en',
+        'title' => 'node title 1',
+        'uid' => 1,
+        'status' => 1,
+        'created' => 1279051598,
+        'changed' => 1279051598,
+        'comment' => 2,
+        'promote' => 1,
+        'moderate' => 0,
+        'sticky' => 0,
+        'translate' => 0,
+        'tnid' => 0,
+      ],
+      [
+        'nid' => 2,
+        'vid' => 2,
+        'type' => 'page',
+        'language' => 'en',
+        'title' => 'node title 2',
+        'uid' => 1,
+        'status' => 1,
+        'created' => 1279290908,
+        'changed' => 1279308993,
+        'comment' => 0,
+        'promote' => 1,
+        'moderate' => 0,
+        'sticky' => 0,
+        'translate' => 0,
+        'tnid' => 0,
+      ],
+      [
+        'nid' => 5,
+        'vid' => 5,
+        'type' => 'story',
+        'language' => 'en',
+        'title' => 'node title 5',
+        'uid' => 1,
+        'status' => 1,
+        'created' => 1279290908,
+        'changed' => 1279308993,
+        'comment' => 0,
+        'promote' => 1,
+        'moderate' => 0,
+        'sticky' => 0,
+        'translate' => 0,
+        'tnid' => 0,
+      ],
+      [
+        'nid' => 6,
+        'vid' => 6,
+        'type' => 'story',
+        'language' => 'en',
+        'title' => 'node title 6',
+        'uid' => 1,
+        'status' => 1,
+        'created' => 1279290909,
+        'changed' => 1279308994,
+        'comment' => 0,
+        'promote' => 1,
+        'moderate' => 0,
+        'sticky' => 0,
+        'translate' => 0,
+        'tnid' => 6,
+      ],
+      [
+        'nid' => 7,
+        'vid' => 7,
+        'type' => 'story',
+        'language' => 'fr',
+        'title' => 'node title 7',
+        'uid' => 1,
+        'status' => 1,
+        'created' => 1279290910,
+        'changed' => 1279308995,
+        'comment' => 0,
+        'promote' => 1,
+        'moderate' => 0,
+        'sticky' => 0,
+        'translate' => 0,
+        'tnid' => 6,
+      ],
+    ];
+
+    foreach ($this->databaseContents['node'] as $k => $row) {
+      // Find the equivalent row from expected results.
+      $result_row = NULL;
+      foreach ($this->expectedResults as $result) {
+        if (in_array($result['nid'], [$row['nid'], $row['tnid']]) && $result['language'] == $row['language']) {
+          $result_row = $result;
+          break;
+        }
+      }
+
+      // Populate node_revisions.
+      foreach (array('nid', 'vid', 'title', 'uid', 'body', 'teaser', 'format', 'timestamp', 'log') as $field) {
+        $value = isset($row[$field]) ? $row[$field] : $result_row[$field];
+        $this->databaseContents['node_revisions'][$k][$field] = $value;
+        if ($field == 'uid') {
+          $this->databaseContents['node_revisions'][$k]['uid']++;
+        }
+      }
+    }
+
+    array_walk($this->expectedResults, function (&$row) {
+      $row['node_uid'] = $row['uid'];
+      $row['revision_uid'] = $row['uid'] + 1;
+      unset($row['uid']);
+    });
+
+    parent::setUp();
+  }
+
+}
diff --git a/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeTranslationTest.php b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeTranslationTest.php
new file mode 100644
index 0000000..da0b167
--- /dev/null
+++ b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d6/NodeTranslationTest.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\Tests\node\Unit\Plugin\migrate\source\d6;
+
+/**
+ * Tests D6 node translation source plugin.
+ *
+ * @group node
+ */
+class NodeTranslationTest extends NodeTestBase {
+
+  protected $migrationConfiguration = array(
+    'id' => 'test',
+    'source' => array(
+      'plugin' => 'd6_node',
+      'translations' => TRUE,
+    ),
+  );
+
+  protected $expectedResults = array(
+    array(
+      'nid' => 7,
+      'vid' => 7,
+      'type' => 'story',
+      'language' => 'fr',
+      'title' => 'node title 7',
+      'uid' => 1,
+      'status' => 1,
+      'created' => 1279290910,
+      'changed' => 1279308995,
+      'comment' => 0,
+      'promote' => 1,
+      'moderate' => 0,
+      'sticky' => 0,
+      'tnid' => 6,
+      'translate' => 0,
+      // Node revision fields.
+      'body' => 'body for node 7',
+      'teaser' => 'body for node 7',
+      'log' => '',
+      'timestamp' => 1279308995,
+      'format' => 1,
+    ),
+  );
+
+}
diff --git a/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeRevisionTest.php b/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeRevisionTest.php
index ff2b514..facd69b 100644
--- a/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeRevisionTest.php
+++ b/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeRevisionTest.php
@@ -22,7 +22,7 @@ class MigrateTermNodeRevisionTest extends MigrateDrupal6TestBase {
   protected function setUp() {
     parent::setUp();
     $this->installSchema('node', ['node_access']);
-    $this->migrateContent(TRUE);
+    $this->migrateContent(['revisions']);
     $this->migrateTaxonomy();
     $this->executeMigrations(['d6_term_node', 'd6_term_node_revision']);
   }
diff --git a/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTest.php b/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTest.php
index 718c35d..26f069c 100644
--- a/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTest.php
+++ b/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTest.php
@@ -55,7 +55,7 @@ public function testTermNode() {
   public function testSkipNonExistentNode() {
     // Node 2 is migrated by d6_node__story, but we need to pretend that it
     // failed, so record that in the map table.
-    $this->mockFailure('d6_node:story', ['nid' => 2]);
+    $this->mockFailure('d6_node:story', ['nid' => 2, 'language' => 'en']);
 
     // d6_term_node__2 should skip over node 2 (a.k.a. revision 3) because,
     // according to the map table, it failed.
