diff --git a/core/modules/book/tests/src/Kernel/Migrate/d6/MigrateBookTest.php b/core/modules/book/tests/src/Kernel/Migrate/d6/MigrateBookTest.php
index ffcc3fe..2e69972 100644
--- a/core/modules/book/tests/src/Kernel/Migrate/d6/MigrateBookTest.php
+++ b/core/modules/book/tests/src/Kernel/Migrate/d6/MigrateBookTest.php
@@ -15,7 +15,7 @@ class MigrateBookTest extends MigrateDrupal6TestBase {
   /**
    * {@inheritdoc}
    */
-  public static $modules = ['book'];
+  public static $modules = ['language', 'book'];
 
   /**
    * {@inheritdoc}
diff --git a/core/modules/comment/tests/src/Kernel/Migrate/d6/MigrateCommentTest.php b/core/modules/comment/tests/src/Kernel/Migrate/d6/MigrateCommentTest.php
index 5330e94..cd88ba1 100644
--- a/core/modules/comment/tests/src/Kernel/Migrate/d6/MigrateCommentTest.php
+++ b/core/modules/comment/tests/src/Kernel/Migrate/d6/MigrateCommentTest.php
@@ -17,7 +17,7 @@ class MigrateCommentTest extends MigrateDrupal6TestBase {
   /**
    * {@inheritdoc}
    */
-  public static $modules = ['comment'];
+  public static $modules = ['language', 'comment'];
 
   /**
    * {@inheritdoc}
diff --git a/core/modules/file/tests/src/Kernel/Migrate/d6/MigrateUploadTest.php b/core/modules/file/tests/src/Kernel/Migrate/d6/MigrateUploadTest.php
index 463ce02..0de0007 100644
--- a/core/modules/file/tests/src/Kernel/Migrate/d6/MigrateUploadTest.php
+++ b/core/modules/file/tests/src/Kernel/Migrate/d6/MigrateUploadTest.php
@@ -16,6 +16,11 @@ class MigrateUploadTest extends MigrateDrupal6TestBase {
   /**
    * {@inheritdoc}
    */
+  public static $modules = ['language'];
+
+  /**
+   * {@inheritdoc}
+   */
   protected function setUp() {
     parent::setUp();
 
diff --git a/core/modules/migrate/src/Plugin/migrate/destination/Entity.php b/core/modules/migrate/src/Plugin/migrate/destination/Entity.php
index 55c59fc..a534851 100644
--- a/core/modules/migrate/src/Plugin/migrate/destination/Entity.php
+++ b/core/modules/migrate/src/Plugin/migrate/destination/Entity.php
@@ -109,7 +109,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 {
       // Stubs might need some required fields filled in.
diff --git a/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php b/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php
index 6a2fb98..9430628 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,40 @@ 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['source_language_keys'])) {
+      $ids[] = $entity->language()->getId();
+    }
+    return $ids;
+  }
+
+  /**
+   * Get the destination ID values of a translation of this entity.
+   *
+   * @param \Drupal\migrate\Row $row
+   *   The row object.
+   * @param array &$destination_ids
+   *   The old destination IDs, to be modified by this method.
+   */
+  protected function getTranslatedDestinationIds(Row $row, array &$destination_ids) {
+    if (empty($destination_ids) && isset($this->configuration['source_language_keys'])) {
+      // Get source IDs, without language keys.
+      $source_language_keys = (array) $this->configuration['source_language_keys'];
+      $source_ids = array_diff_key($row->getSourceIdValues(), array_flip($source_language_keys));
+
+      // Try to lookup another translation of the same entity.
+      $dest_ids = $this->migration->getIdMap()->lookupDestinationIds($source_ids);
+      $destination_ids = reset($dest_ids) ?: [];
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEntity(Row $row, array $old_destination_id_values) {
+    $this->getTranslatedDestinationIds($row, $old_destination_id_values);
+    return parent::getEntity($row, $old_destination_id_values);
   }
 
   /**
@@ -105,14 +139,36 @@ protected function save(ContentEntityInterface $entity, array $old_destination_i
   }
 
   /**
-   * {@inheritdoc}
+   * Get the base IDs of this entity, not including dynamically determined IDs.
+   *
+   * @return array
+   *   An array of IDs.
    */
-  public function getIds() {
+  protected function baseIds() {
     $id_key = $this->getKey('id');
     $ids[$id_key]['type'] = 'integer';
     return $ids;
   }
 
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIds() {
+    $ids = $this->baseIds();
+
+    if (!empty($this->configuration['source_language_keys'])) {
+      if ($key = $this->getKey('langcode')) {
+        $ids[$key]['type'] = 'string';
+      }
+      else {
+        throw new MigrateException('This entity type does not support translation.');
+      }
+    }
+
+    return $ids;
+  }
+
   /**
    * Updates an entity with the new values from row.
    *
@@ -122,6 +178,18 @@ public function getIds() {
    *   The row object to update from.
    */
   protected function updateEntity(EntityInterface $entity, Row $row) {
+    // Make sure we have the right translation.
+    if ($entity instanceof TranslatableInterface) {
+      $property = $this->storage->getEntityType()->getKey('langcode');
+      if ($row->hasDestinationProperty($property)) {
+        $language = $row->getDestinationProperty($property);
+        if (!$entity->hasTranslation($language)) {
+          $entity->addTranslation($language);
+        }
+        $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.
@@ -141,6 +209,9 @@ protected function updateEntity(EntityInterface $entity, Row $row) {
     }
 
     $this->setRollbackAction($row->getIdMap());
+
+    // We might have a different (translated) entity, so return it.
+    return $entity;
   }
 
   /**
diff --git a/core/modules/migrate_drupal/tests/fixtures/drupal6.php b/core/modules/migrate_drupal/tests/fixtures/drupal6.php
index 1ca67a5..9a55470 100644
--- a/core/modules/migrate_drupal/tests/fixtures/drupal6.php
+++ b/core/modules/migrate_drupal/tests/fixtures/drupal6.php
@@ -34727,7 +34727,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' => '',
@@ -34749,7 +34749,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' => '',
@@ -34859,7 +34859,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',
@@ -41338,18 +41338,35 @@
 ->values(array(
   'nid' => '9',
   'vid' => '12',
-  'type' => 'story',
-  'language' => '',
-  'title' => 'Once upon a time',
+  'type' => 'article',
+  'language' => 'en',
+  'title' => 'The Real McCoy',
   'uid' => '1',
   'status' => '1',
-  'created' => '1444671588',
-  'changed' => '1444671588',
+  'created' => '1444238800',
+  'changed' => '1444238808',
   'comment' => '2',
   'promote' => '1',
   'moderate' => '0',
   'sticky' => '0',
-  'tnid' => '0',
+  'tnid' => '9',
+  'translate' => '0',
+))
+->values(array(
+  'nid' => '10',
+  'vid' => '13',
+  'type' => 'article',
+  'language' => 'fr',
+  'title' => 'Le Vrai McCoy',
+  'uid' => '1',
+  'status' => '1',
+  'created' => '1444239050',
+  'changed' => '1444239050',
+  'comment' => '2',
+  'promote' => '1',
+  'moderate' => '0',
+  'sticky' => '0',
+  'tnid' => '9',
   'translate' => '0',
 ))
 ->execute();
@@ -41497,7 +41514,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',
@@ -41738,11 +41762,22 @@
   'nid' => '9',
   'vid' => '12',
   'uid' => '1',
-  'title' => 'Once upon a time',
-  'body' => 'Come on kid, go to sleep.',
-  'teaser' => 'Come on kid, go to sleep.',
+  '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' => '10',
+  'vid' => '13',
+  'uid' => '1',
+  'title' => 'Le Vrai McCoy',
+  'body' => 'Ooh là là!',
+  'teaser' => 'Ooh là là!',
   'log' => '',
-  'timestamp' => '1444671588',
+  'timestamp' => '1444239050',
   'format' => '1',
 ))
 ->execute();
@@ -41879,9 +41914,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',
 ))
@@ -44828,12 +44863,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',
@@ -44873,7 +44912,7 @@
 ))
 ->values(array(
   'name' => 'comment_default_mode_company',
-  'value' => 'i:4;',
+  'value' => 's:1:"4";',
 ))
 ->values(array(
   'name' => 'comment_default_mode_employee',
@@ -44913,7 +44952,7 @@
 ))
 ->values(array(
   'name' => 'comment_default_order_company',
-  'value' => 'i:1;',
+  'value' => 's:1:"1";',
 ))
 ->values(array(
   'name' => 'comment_default_order_employee',
@@ -44953,7 +44992,7 @@
 ))
 ->values(array(
   'name' => 'comment_default_per_page_company',
-  'value' => 'i:50;',
+  'value' => 's:2:"50";',
 ))
 ->values(array(
   'name' => 'comment_default_per_page_employee',
@@ -44993,7 +45032,7 @@
 ))
 ->values(array(
   'name' => 'comment_form_location_company',
-  'value' => 'i:0;',
+  'value' => 's:1:"0";',
 ))
 ->values(array(
   'name' => 'comment_form_location_employee',
@@ -45037,7 +45076,7 @@
 ))
 ->values(array(
   'name' => 'comment_preview_company',
-  'value' => 'i:1;',
+  'value' => 's:1:"1";',
 ))
 ->values(array(
   'name' => 'comment_preview_employee',
@@ -45081,7 +45120,7 @@
 ))
 ->values(array(
   'name' => 'comment_subject_field_company',
-  'value' => 'i:1;',
+  'value' => 's:1:"1";',
 ))
 ->values(array(
   'name' => 'comment_subject_field_employee',
@@ -45444,6 +45483,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";',
 ))
@@ -45505,7 +45548,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',
@@ -45600,6 +45647,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";}',
 ))
@@ -45796,6 +45847,10 @@
   'value' => 'b:0;',
 ))
 ->values(array(
+  'name' => 'upload_company',
+  'value' => 's:1:"1";',
+))
+->values(array(
   'name' => 'upload_page',
   'value' => 'b:1;',
 ))
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 761e718..476ea38 100644
--- a/core/modules/migrate_drupal/tests/src/Kernel/d6/MigrateDrupal6TestBase.php
+++ b/core/modules/migrate_drupal/tests/src/Kernel/d6/MigrateDrupal6TestBase.php
@@ -86,6 +86,7 @@ protected function migrateFields() {
    *   If TRUE, migrates node revisions.
    */
   protected function migrateContent($include_revisions = FALSE) {
+    $this->executeMigrations(['language']);
     $this->migrateUsers(FALSE);
     $this->migrateFields();
 
diff --git a/core/modules/migrate_drupal_ui/src/Tests/MigrateUpgradeTestBase.php b/core/modules/migrate_drupal_ui/src/Tests/MigrateUpgradeTestBase.php
index 5398c48..a76bdc2 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', '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 d073c18..ed86ab3 100644
--- a/core/modules/migrate_drupal_ui/src/Tests/d6/MigrateUpgrade6Test.php
+++ b/core/modules/migrate_drupal_ui/src/Tests/d6/MigrateUpgrade6Test.php
@@ -39,8 +39,9 @@ protected function getEntityCounts() {
       'comment' => 3,
       'comment_type' => 2,
       'contact_form' => 5,
+      'configurable_language' => 5,
       'editor' => 2,
-      'field_config' => 62,
+      'field_config' => 63,
       'field_storage_config' => 43,
       'file' => 7,
       'filter_format' => 7,
@@ -56,7 +57,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 084f8ba..a1796ac 100644
--- a/core/modules/migrate_drupal_ui/src/Tests/d7/MigrateUpgrade7Test.php
+++ b/core/modules/migrate_drupal_ui/src/Tests/d7/MigrateUpgrade7Test.php
@@ -38,6 +38,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,
@@ -56,7 +58,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 82571b8..0b61a6b 100644
--- a/core/modules/node/migration_templates/d6_node.yml
+++ b/core/modules/node/migration_templates/d6_node.yml
@@ -38,6 +38,7 @@ process:
 
 destination:
   plugin: entity:node
+  source_language_keys: language
 migration_dependencies:
   required:
     - d6_user
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 c70fd27..d39c486 100644
--- a/core/modules/node/src/Plugin/migrate/source/d6/Node.php
+++ b/core/modules/node/src/Plugin/migrate/source/d6/Node.php
@@ -15,6 +15,11 @@
 class Node extends DrupalSqlBase {
 
   /**
+   * An expression for the translation set of a node.
+   */
+  const NODE_TRANSLATION_SET = 'CASE n.tnid WHEN 0 THEN n.nid ELSE n.tnid END';
+
+  /**
    * The join options between the node and the node_revisions table.
    */
   const JOIN = 'n.vid = nr.vid';
@@ -37,9 +42,9 @@ 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->translationQuery();
+
+    $query->fields('n', array(
         'nid',
         'type',
         'language',
@@ -54,7 +59,6 @@ public function query() {
         'translate',
       ))
       ->fields('nr', array(
-        'vid',
         'title',
         'body',
         'teaser',
@@ -64,10 +68,13 @@ public function query() {
       ));
     $query->addField('n', 'uid', 'node_uid');
     $query->addField('nr', 'uid', 'revision_uid');
-    $query->innerJoin('node', 'n', static::JOIN);
+
+    // Whatever we claim this revision is, use the actual field from
+    // node_revisions to get field values.
+    $query->addField('nr', 'vid', 'vid_for_fields');
 
     if (isset($this->configuration['node_type'])) {
-      $query->condition('type', $this->configuration['node_type']);
+      $query->condition('n.type', $this->configuration['node_type']);
     }
 
     return $query;
@@ -123,6 +130,11 @@ public function prepareRow(Row $row) {
       }
     }
 
+    // Use the same nid for translation sets.
+    if ($tnid = $row->getSourceProperty('tnid')) {
+      $row->setSourceProperty('nid', $tnid);
+    }
+
     return parent::prepareRow($row);
   }
 
@@ -233,7 +245,7 @@ protected function getCckData(array $field, Row $node) {
         // the time being.
         ->isNotNull($field['field_name'] . '_' . $columns[0])
         ->condition('nid', $node->getSourceProperty('nid'))
-        ->condition('vid', $node->getSourceProperty('vid'))
+        ->condition('vid', $node->getSourceProperty('vid_for_fields'))
         ->execute()
         ->fetchAllAssoc('delta');
     }
@@ -248,7 +260,44 @@ protected function getCckData(array $field, Row $node) {
   public function getIds() {
     $ids['nid']['type'] = 'integer';
     $ids['nid']['alias'] = 'n';
+    $ids['language']['type'] = 'string';
+    $ids['language']['alias'] = 'n';
     return $ids;
   }
 
+  /**
+   * Build a query to get the maximum vid of each translation set.
+   *
+   * @return \Drupal\Core\Database\Query\SelectInterface
+   *   The generated query.
+   */
+  protected function maxVidQuery() {
+    $subquery = $this->select('node', 'n');
+    $subquery->addExpression(self::NODE_TRANSLATION_SET, 'translation_set');
+    $subquery->groupBy('translation_set');
+    $subquery->addExpression('MAX(vid)', 'vid');
+    return $subquery;
+  }
+
+  /**
+   * Build a query that yields the rows we want to migrate.
+   *
+   * It should have a node table 'n', and a node_revision table 'nr'.
+   *
+   * @return \Drupal\Core\Database\Query\SelectInterface
+   *   The generated query.
+   */
+  protected function translationQuery() {
+    $query = $this->select('node_revisions', 'nr');
+    $query->innerJoin('node', 'n', static::JOIN);
+
+    // Claim our vid is the maximum vid of our translation set.
+    // Otherwise we generate translations of the same node with different
+    // revisions, which confuses Drupal.
+    $query->join($this->maxVidQuery(), 'max_vid',
+      'max_vid.translation_set IN (n.nid, n.tnid)');
+    $query->fields('max_vid', ['vid']);
+
+    return $query;
+  }
 }
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..e6894d4 100644
--- a/core/modules/node/src/Plugin/migrate/source/d6/NodeRevision.php
+++ b/core/modules/node/src/Plugin/migrate/source/d6/NodeRevision.php
@@ -37,4 +37,14 @@ public function getIds() {
     return $ids;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function translationQuery() {
+    $query = $this->select('node_revisions', 'nr');
+    $query->innerJoin('node', 'n', static::JOIN);
+    $query->addField('nr', 'vid');
+    return $query;
+  }
+
 }
diff --git a/core/modules/node/src/Tests/Migrate/d6/MigrateNodeRevisionTest.php b/core/modules/node/src/Tests/Migrate/d6/MigrateNodeRevisionTest.php
index 05be99f..0311841 100644
--- a/core/modules/node/src/Tests/Migrate/d6/MigrateNodeRevisionTest.php
+++ b/core/modules/node/src/Tests/Migrate/d6/MigrateNodeRevisionTest.php
@@ -15,7 +15,7 @@ class MigrateNodeRevisionTest extends MigrateNodeTestBase {
    */
   protected function setUp() {
     parent::setUp();
-    $this->executeMigrations(['d6_node', 'd6_node_revision']);
+    $this->executeMigrations(['language', 'd6_node', 'd6_node_revision']);
   }
 
   /**
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 7f07d11..1f78d3a 100644
--- a/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeTest.php
+++ b/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeTest.php
@@ -5,6 +5,7 @@
 use Drupal\Core\Database\Database;
 use Drupal\migrate\Plugin\MigrateIdMapInterface;
 use Drupal\node\Entity\Node;
+use Drupal\node\NodeInterface;
 use Drupal\Tests\file\Kernel\Migrate\d6\FileMigrationTestTrait;
 
 /**
@@ -23,7 +24,7 @@ protected function setUp() {
     parent::setUp();
     $this->setUpMigratedFiles();
     $this->installSchema('file', ['file_usage']);
-    $this->executeMigrations(['d6_node']);
+    $this->executeMigrations(['language', 'd6_node']);
   }
 
   /**
@@ -79,6 +80,15 @@ public function testNode() {
     $this->assertIdentical('Drupal Groups', $node->field_test_link->title);
     $this->assertIdentical([], $node->field_test_link->options['attributes']);
 
+    // Test that translations are working.
+    $node = Node::load(9);
+    $this->assertTrue($node instanceof NodeInterface);
+    $this->assertIdentical('en', $node->langcode->value);
+    $this->assertIdentical('The Real McCoy', $node->title->value);
+
+    // Node 10 is a translation of node 9, and should not be imported separately.
+    $this->assertNull(Node::load(10));
+
     // Rerun migration with invalid link attributes and a different URL and
     // title. If only the attributes are changed the error does not occur.
     Database::getConnection('default', 'migrate')
@@ -109,7 +119,9 @@ public function testNode() {
     // Now insert a row indicating a failure and set to update later.
     $title = $this->rerunMigration(array(
       'sourceid1' => 2,
+      'sourceid2' => 'en',
       'destid1' => NULL,
+      'destid2' => NULL,
       'source_row_status' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
     ));
     $node = Node::load(2);
@@ -140,7 +152,10 @@ protected function rerunMigration($new_row = []) {
     $default_connection = \Drupal::database();
     $default_connection->truncate($table_name)->execute();
     if ($new_row) {
-      $hash = $migration->getIdMap()->getSourceIDsHash(['nid' => $new_row['sourceid1']]);
+      $hash = $migration->getIdMap()->getSourceIDsHash([
+        'nid' => $new_row['sourceid1'],
+        'language' => $new_row['sourceid2']
+      ]);
       $new_row['source_ids_hash'] = $hash;
       $default_connection->insert($table_name)
         ->fields($new_row)
diff --git a/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeTestBase.php b/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeTestBase.php
index e225003..ca21ad7 100644
--- a/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeTestBase.php
+++ b/core/modules/node/tests/src/Kernel/Migrate/d6/MigrateNodeTestBase.php
@@ -13,6 +13,11 @@
   /**
    * {@inheritdoc}
    */
+  public static $modules = ['language'];
+
+  /**
+   * {@inheritdoc}
+   */
   protected function setUp() {
     parent::setUp();
 
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 79f2bac..80f75b0 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
@@ -95,6 +95,52 @@ class NodeTest extends MigrateSqlSourceTestCase {
         array('value' => '3.14159'),
       ),
     ),
+    array(
+      'nid' => 6,
+      'vid' => 7,
+      '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,
+    ),
+    array(
+      'nid' => 6,
+      '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,
+    ),
   );
 
   /**
@@ -145,22 +191,114 @@ protected function setUp() {
         'status' => TRUE,
       ),
     );
-    foreach ($this->expectedResults as $k => $row) {
+    $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) {
-        $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;
+        $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']++;
         }
       }
-      $this->databaseContents['node'][$k] = $row;
     }
+
     array_walk($this->expectedResults, function (&$row) {
       $row['node_uid'] = $row['uid'];
       $row['revision_uid'] = $row['uid'] + 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..ac9f881 100644
--- a/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeRevisionTest.php
+++ b/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeRevisionTest.php
@@ -14,7 +14,7 @@ class MigrateTermNodeRevisionTest extends MigrateDrupal6TestBase {
   /**
    * {@inheritdoc}
    */
-  public static $modules = ['taxonomy'];
+  public static $modules = ['language', 'taxonomy'];
 
   /**
    * {@inheritdoc}
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..40be413 100644
--- a/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTest.php
+++ b/core/modules/taxonomy/tests/src/Kernel/Migrate/d6/MigrateTermNodeTest.php
@@ -15,7 +15,7 @@ class MigrateTermNodeTest extends MigrateDrupal6TestBase {
   /**
    * {@inheritdoc}
    */
-  public static $modules = ['taxonomy'];
+  public static $modules = ['language', 'taxonomy'];
 
   /**
    * {@inheritdoc}
diff --git a/core/modules/user/migration_templates/d7_user.yml b/core/modules/user/migration_templates/d7_user.yml
index 12147f8..5e9dcc7 100644
--- a/core/modules/user/migration_templates/d7_user.yml
+++ b/core/modules/user/migration_templates/d7_user.yml
@@ -15,7 +15,6 @@ process:
   login: login
   status: status
   timezone: timezone
-  langcode: language
   preferred_langcode: language
   preferred_admin_langcode: language
   init: init
