diff --git a/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php
index 07d6a05..5da41eb 100644
--- a/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php
+++ b/core/modules/migrate_drupal_ui/src/Form/MigrateUpgradeForm.php
@@ -346,6 +346,10 @@ class MigrateUpgradeForm extends ConfirmFormBase {
       'source_module' => 'node',
       'destination_module' => 'node',
     ],
+    'd7_node_translation' => [
+      'source_module' => 'node',
+      'destination_module' => 'node',
+    ],
     'd7_node_title_label' => [
       'source_module' => 'node',
       'destination_module' => 'node',
diff --git a/core/modules/node/migration_templates/d7_node.yml b/core/modules/node/migration_templates/d7_node.yml
index b763534..a056bf2 100644
--- a/core/modules/node/migration_templates/d7_node.yml
+++ b/core/modules/node/migration_templates/d7_node.yml
@@ -6,7 +6,10 @@ deriver: Drupal\node\Plugin\migrate\D7NodeDeriver
 source:
   plugin: d7_node
 process:
-  nid: nid
+  # In D7, 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\d7\Node::prepareRow().
+  nid: tnid
   vid: vid
   langcode:
     plugin: default_value
diff --git a/core/modules/node/migration_templates/d7_node_translation.yml b/core/modules/node/migration_templates/d7_node_translation.yml
new file mode 100644
index 0000000..2577a09
--- /dev/null
+++ b/core/modules/node/migration_templates/d7_node_translation.yml
@@ -0,0 +1,36 @@
+qid: d7_node_translation
+label: Node translations
+migration_tags:
+  - Drupal 7
+deriver: Drupal\node\Plugin\migrate\D7NodeDeriver
+source:
+  plugin: d7_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
+  revision_uid: revision_uid
+  revision_log: log
+  revision_timestamp: timestamp
+destination:
+  plugin: entity:node
+  translations: true
+migration_dependencies:
+  required:
+    - d7_user
+    - d7_node_type
+    - language
+  optional:
+    - d7_field_instance
+provider: migrate_drupal
diff --git a/core/modules/node/src/Plugin/migrate/D7NodeDeriver.php b/core/modules/node/src/Plugin/migrate/D7NodeDeriver.php
index bd3d8b9..fcac62f 100644
--- a/core/modules/node/src/Plugin/migrate/D7NodeDeriver.php
+++ b/core/modules/node/src/Plugin/migrate/D7NodeDeriver.php
@@ -39,33 +39,58 @@ class D7NodeDeriver extends DeriverBase implements ContainerDeriverInterface {
   protected $cckPluginManager;
 
   /**
-   * D7NodeDeriver constructor.
+   * Whether or not to include translations.
    *
+   * @var bool
+   */
+  protected $includeTranslations;
+
+  /**
+   * D7NodeDeriver constructor.
    * @param string $base_plugin_id
    *   The base plugin ID for the plugin ID.
    * @param \Drupal\migrate_drupal\Plugin\MigrateCckFieldPluginManagerInterface $cck_manager
    *   The CCK plugin manager.
+   * @param bool $translations
+   *   Whether or not to include translations.
    */
-  public function __construct($base_plugin_id, MigrateCckFieldPluginManagerInterface $cck_manager) {
+  public function __construct($base_plugin_id, MigrateCckFieldPluginManagerInterface $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')
     );
   }
 
   /**
-   * {@inheritdoc}
+   * Gets the definition of all derivatives of a base plugin.
+   *
+   * @param array $base_plugin_definition
+   *   The definition array of the base plugin.
+   *
+   * @return array
+   *   An array of full derivative definitions keyed on derivative id.
+   *
+   * @see \Drupal\Component\Plugin\Derivative\DeriverBase::getDerivativeDefinition()
    */
   public function getDerivativeDefinitions($base_plugin_definition) {
-    $fields = [];
+    if ($base_plugin_definition['id'] == 'd7_node_translation' && !$this->includeTranslations) {
+      // Refuse to generate anything.
+      return $this->derivatives;
+    }
+
+    // Read all CCK field instance definitions in the source database.
+    $fields = array();
     try {
       $source_plugin = static::getSourcePlugin('d7_field_instance');
       $source_plugin->checkRequirements();
@@ -95,6 +120,13 @@ 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 d7_node_revision migration or
+        // is for translations of nodes, it should explicitly depend on the
+        // corresponding d7_node variant.
+        if (in_array($base_plugin_definition['id'], ['d7_node_revision', 'd7_node_translation'])) {
+          $values['migration_dependencies']['required'][] = 'd7_node:' . $node_type;
+        }
+
         $migration = \Drupal::service('plugin.manager.migration')->createStubMigration($values);
         if (isset($fields[$node_type])) {
           foreach ($fields[$node_type] as $field_name => $info) {
@@ -121,7 +153,6 @@ public function getDerivativeDefinitions($base_plugin_definition) {
       // MigrationPluginManager gathers up the migration definitions but we do
       // not actually have a Drupal 7 source database.
     }
-
     return $this->derivatives;
   }
 
diff --git a/core/modules/node/src/Plugin/migrate/source/d7/Node.php b/core/modules/node/src/Plugin/migrate/source/d7/Node.php
index 6286d5f..87dddde 100644
--- a/core/modules/node/src/Plugin/migrate/source/d7/Node.php
+++ b/core/modules/node/src/Plugin/migrate/source/d7/Node.php
@@ -4,6 +4,7 @@
 
 use Drupal\migrate\Row;
 use Drupal\migrate_drupal\Plugin\migrate\source\d7\FieldableEntity;
+use Drupal\Core\Database\Query\SelectInterface;
 
 /**
  * Drupal 7 node source from database.
@@ -49,6 +50,8 @@ public function query() {
     $query->addField('nr', 'uid', 'revision_uid');
     $query->innerJoin('node', 'n', static::JOIN);
 
+    $this->handleTranslations($query);
+
     if (isset($this->configuration['node_type'])) {
       $query->condition('n.type', $this->configuration['node_type']);
     }
@@ -66,6 +69,11 @@ public function prepareRow(Row $row) {
       $vid = $row->getSourceProperty('vid');
       $row->setSourceProperty($field, $this->getFieldValues('node', $field, $nid, $vid));
     }
+
+    // Make sure we always have a translation set.
+    if ($row->getSourceProperty('tnid') == 0) {
+      $row->setSourceProperty('tnid', $row->getSourceProperty('nid'));
+    }
     return parent::prepareRow($row);
   }
 
@@ -101,4 +109,21 @@ public function getIds() {
     return $ids;
   }
 
+  /**
+   * Adapt our query for translations.
+   *
+   * @param \Drupal\Core\Database\Query\SelectInterface $query
+   *   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/tests/src/Kernel/Migrate/d7/MigrateNodeDeriverTest.php b/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeDeriverTest.php
new file mode 100644
index 0000000..e51a40c
--- /dev/null
+++ b/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeDeriverTest.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\Tests\node\Kernel\Migrate\d7;
+
+use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
+
+/**
+ * Test D7NodeDeriver.
+ *
+ * @group migrate_drupal_7
+ */
+class MigrateNodeDeriverTest extends MigrateDrupal7TestBase {
+  /**
+   * 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('d7_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', 'node', 'filter']);
+    $migrations = $this->pluginManager->createInstances('d7_node_translation');
+    $this->assertArrayHasKey('d7_node_translation:article', $migrations,
+      "Node translation migrations exist after content_translation installed");
+  }
+
+}
diff --git a/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeTest.php b/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeTest.php
index f7b98bd..df56f82 100644
--- a/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeTest.php
+++ b/core/modules/node/tests/src/Kernel/Migrate/d7/MigrateNodeTest.php
@@ -14,10 +14,12 @@
 class MigrateNodeTest extends MigrateDrupal7TestBase {
 
   public static $modules = array(
+    'content_translation',
     'comment',
     'datetime',
     'filter',
     'image',
+    'language',
     'link',
     'node',
     'taxonomy',
@@ -40,6 +42,7 @@ protected function setUp() {
     $this->installSchema('system', ['sequences']);
 
     $this->executeMigrations([
+      'language',
       'd7_user_role',
       'd7_user',
       'd7_node_type',
@@ -48,7 +51,7 @@ protected function setUp() {
       'd7_field',
       'd7_field_instance',
       'd7_node',
-      'd7_node:article',
+      'd7_node_translation',
     ]);
   }
 
@@ -144,9 +147,15 @@ public function testNode() {
     $this->assertIdentical('Click Here', $node->field_link->title);
 
     $node = Node::load(2);
+    $this->assertIdentical('en', $node->langcode->value);
     $this->assertIdentical("...is that it's the absolute best show ever. Trust me, I would know.", $node->body->value);
+    $this->assertIdentical('The thing about Deep Space 9', $node->title->value);
     $this->assertIdentical('internal:/', $node->field_link->uri);
     $this->assertIdentical('Home', $node->field_link->title);
+    $this->assertTrue($node->hasTranslation('is'), "Node 2 has an Icelandic translation");
+
+    // Node 3 is a translation of node 2, and should not be imported separately.
+    $this->assertNull(Node::load(3), "Node 3 doesn't exist in D8, it was a translation");
   }
 
 }
diff --git a/core/modules/node/tests/src/Unit/Plugin/migrate/source/d7/NodeTest.php b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d7/NodeTest.php
index b0489bf..b15e453 100644
--- a/core/modules/node/tests/src/Unit/Plugin/migrate/source/d7/NodeTest.php
+++ b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d7/NodeTest.php
@@ -9,7 +9,7 @@
  *
  * @group node
  */
-class NodeTest extends MigrateSqlSourceTestCase {
+class NodeTest extends NodeTestBase {
 
   const PLUGIN_CLASS = 'Drupal\node\Plugin\migrate\source\d7\Node';
 
@@ -35,8 +35,9 @@ class NodeTest extends MigrateSqlSourceTestCase {
       'comment' => 2,
       'promote' => 1,
       'sticky' => 0,
-      'tnid' => 0,
+      'tnid' => 1,
       'translate' => 0,
+      // Node revision fields.
       'log' => '',
       'timestamp' => 1279051598,
     ),
@@ -54,8 +55,9 @@ class NodeTest extends MigrateSqlSourceTestCase {
       'comment' => 0,
       'promote' => 1,
       'sticky' => 0,
-      'tnid' => 0,
+      'tnid' => 2,
       'translate' => 0,
+      // Node revision fields.
       'log' => '',
       'timestamp' => 1279308993,
     ),
@@ -73,75 +75,31 @@ class NodeTest extends MigrateSqlSourceTestCase {
       'comment' => 0,
       'promote' => 1,
       'sticky' => 0,
-      'tnid' => 0,
+      'tnid' => 5,
       'translate' => 0,
+      // Node revision fields.
       'log' => '',
       'timestamp' => 1279308993,
     ),
+    array(
+      // Node fields.
+      'nid' => 6,
+      'vid' => 6,
+      'type' => 'article',
+      'language' => 'en',
+      'title' => 'node title 5',
+      'uid' => 1,
+      'status' => 1,
+      'created' => 1279291908,
+      'changed' => 1279309993,
+      'comment' => 0,
+      'promote' => 1,
+      'sticky' => 0,
+      'tnid' => 6,
+      'translate' => 0,
+      // Node revision fields.
+      'log' => '',
+      'timestamp' => 1279309993,
+    )
   );
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function setUp() {
-    foreach ($this->expectedResults as $k => $row) {
-      foreach (array('nid', 'vid', 'title', 'uid', 'timestamp', 'log') as $field) {
-        $this->databaseContents['node_revision'][$k][$field] = $row[$field];
-        switch ($field) {
-          case 'nid': case 'vid':
-            break;
-          case 'uid':
-            $this->databaseContents['node_revision'][$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']);
-    });
-
-    $this->databaseContents['field_config_instance'] = array(
-      array(
-        'id' => '2',
-        'field_id' => '2',
-        'field_name' => 'body',
-        'entity_type' => 'node',
-        'bundle' => 'page',
-        'data' => 'a:0:{}',
-        'deleted' => '0',
-      ),
-      array(
-        'id' => '2',
-        'field_id' => '2',
-        'field_name' => 'body',
-        'entity_type' => 'node',
-        'bundle' => 'article',
-        'data' => 'a:0:{}',
-        'deleted' => '0',
-      ),
-    );
-    $this->databaseContents['field_revision_body'] = array(
-      array(
-        'entity_type' => 'node',
-        'bundle' => 'page',
-        'deleted' => '0',
-        'entity_id' => '1',
-        'revision_id' => '1',
-        'language' => 'en',
-        'delta' => '0',
-        'body_value' => 'Foobaz',
-        'body_summary' => '',
-        'body_format' => 'filtered_html',
-      ),
-    );
-
-    parent::setUp();
-  }
-
 }
diff --git a/core/modules/node/tests/src/Unit/Plugin/migrate/source/d7/NodeTestBase.php b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d7/NodeTestBase.php
new file mode 100644
index 0000000..8febbd2
--- /dev/null
+++ b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d7/NodeTestBase.php
@@ -0,0 +1,183 @@
+<?php
+
+namespace Drupal\Tests\node\Unit\Plugin\migrate\source\d7;
+
+use Drupal\Tests\migrate\Unit\MigrateSqlSourceTestCase;
+
+/**
+ * Base for D7 node migration tests.
+ */
+abstract class NodeTestBase extends MigrateSqlSourceTestCase {
+
+  const PLUGIN_CLASS = 'Drupal\node\Plugin\migrate\source\d7\Node';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    $this->databaseContents['node'] = [
+      [
+        // Node fields.
+        'nid' => 1,
+        'vid' => 1,
+        'type' => 'page',
+        'language' => 'en',
+        'title' => 'node title 1',
+        'uid' => 1,
+        'status' => 1,
+        'created' => 1279051598,
+        'changed' => 1279051598,
+        'comment' => 2,
+        'promote' => 1,
+        'sticky' => 0,
+        'tnid' => 0,
+        'translate' => 0,
+        'log' => '',
+        'timestamp' => 1279051598,
+      ],
+      [
+        // Node fields.
+        'nid' => 2,
+        'vid' => 2,
+        'type' => 'page',
+        'language' => 'en',
+        'title' => 'node title 2',
+        'uid' => 1,
+        'status' => 1,
+        'created' => 1279290908,
+        'changed' => 1279308993,
+        'comment' => 0,
+        'promote' => 1,
+        'sticky' => 0,
+        'tnid' => 0,
+        'translate' => 0,
+        'log' => '',
+        'timestamp' => 1279308993,
+      ],
+      [
+        // Node fields.
+        'nid' => 5,
+        'vid' => 5,
+        'type' => 'article',
+        'language' => 'en',
+        'title' => 'node title 5',
+        'uid' => 1,
+        'status' => 1,
+        'created' => 1279290908,
+        'changed' => 1279308993,
+        'comment' => 0,
+        'promote' => 1,
+        'sticky' => 0,
+        'tnid' => 0,
+        'translate' => 0,
+        'log' => '',
+        'timestamp' => 1279308993,
+      ],
+      [
+        // Node fields.
+        'nid' => 6,
+        'vid' => 6,
+        'type' => 'article',
+        'language' => 'en',
+        'title' => 'node title 5',
+        'uid' => 1,
+        'status' => 1,
+        'created' => 1279291908,
+        'changed' => 1279309993,
+        'comment' => 0,
+        'promote' => 1,
+        'sticky' => 0,
+        'tnid' => 6,
+        'translate' => 0,
+        'log' => '',
+        'timestamp' => 1279309993,
+      ],
+      [
+        // Node fields.
+        'nid' => 7,
+        'vid' => 7,
+        'type' => 'article',
+        'language' => 'fr',
+        'title' => 'fr - node title 5',
+        'uid' => 1,
+        'status' => 1,
+        'created' => 1279292908,
+        'changed' => 1279310993,
+        'comment' => 0,
+        'promote' => 1,
+        'sticky' => 0,
+        'tnid' => 6,
+        'translate' => 0,
+        'log' => '',
+        'timestamp' => 1279310993,
+      ],
+    ];
+
+    $this->databaseContents['field_config_instance'] = array(
+      array(
+        'id' => '2',
+        'field_id' => '2',
+        'field_name' => 'body',
+        'entity_type' => 'node',
+        'bundle' => 'page',
+        'data' => 'a:0:{}',
+        'deleted' => '0',
+      ),
+      array(
+        'id' => '3',
+        'field_id' => '2',
+        'field_name' => 'body',
+        'entity_type' => 'node',
+        'bundle' => 'article',
+        'data' => 'a:0:{}',
+        'deleted' => '0',
+      ),
+    );
+
+    $this->databaseContents['field_revision_body'] = array(
+      array(
+        'entity_type' => 'node',
+        'bundle' => 'page',
+        'deleted' => '0',
+        'entity_id' => '1',
+        'revision_id' => '1',
+        'language' => 'en',
+        'delta' => '0',
+        'body_value' => 'Foobaz',
+        'body_summary' => '',
+        'body_format' => 'filtered_html',
+      ),
+    );
+
+
+    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 (['nid', 'vid', 'title', 'uid', 'log', 'timestamp', 'status', 'comment','promote', 'sticky'] as $field) {
+        $value = isset($row[$field]) ? $row[$field] : $result_row[$field];
+        $this->databaseContents['node_revision'][$k][$field] = $value;
+        if ($field == 'uid') {
+          $this->databaseContents['node_revision'][$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/d7/NodeTranslationTest.php b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d7/NodeTranslationTest.php
new file mode 100644
index 0000000..b2b77b2
--- /dev/null
+++ b/core/modules/node/tests/src/Unit/Plugin/migrate/source/d7/NodeTranslationTest.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Drupal\Tests\node\Unit\Plugin\migrate\source\d7;
+
+/**
+ * Tests D7 node translation source plugin.
+ *
+ * @group node
+ */
+class NodeTranslationTest extends NodeTestBase {
+
+  protected $migrationConfiguration = [
+    'id' => 'test',
+    'source' => [
+      'plugin' => 'd7_node',
+      'translations' => TRUE,
+    ],
+  ];
+
+  protected $expectedResults = [
+    [
+      // Node fields.
+      'nid' => 7,
+      'vid' => 7,
+      'type' => 'article',
+      'language' => 'fr',
+      'title' => 'fr - node title 5',
+      'uid' => 1,
+      'status' => 1,
+      'created' => 1279292908,
+      'changed' => 1279310993,
+      'comment' => 0,
+      'promote' => 1,
+      'sticky' => 0,
+      'tnid' => 6,
+      'translate' => 0,
+      // Node revision fields.
+      'log' => '',
+      'timestamp' => 1279310993,
+    ],
+  ];
+
+}
