diff --git a/core/modules/file/lib/Drupal/file/Type/FileItem.php b/core/modules/file/lib/Drupal/file/Type/FileItem.php
index e1c7d24..cecce05 100644
--- a/core/modules/file/lib/Drupal/file/Type/FileItem.php
+++ b/core/modules/file/lib/Drupal/file/Type/FileItem.php
@@ -84,7 +84,10 @@ public function setValue($values) {
     else {
       $this->properties['entity']->setValue(NULL);
     }
-    unset($values['entity'], $values['fid'], $values['display'], $values['description']);
+    unset($values['entity'], $values['fid']);
+    // @todo These properties are sometimes set due to being present in form
+    //   values. Needs to be cleaned up somewhere.
+    unset($values['display'], $values['description'], $values['upload']);
     if ($values) {
       throw new \InvalidArgumentException('Property ' . key($values) . ' is unknown.');
     }
diff --git a/core/modules/node/lib/Drupal/node/NodeAccessController.php b/core/modules/node/lib/Drupal/node/NodeAccessController.php
index 815c2ab..35c5c94 100644
--- a/core/modules/node/lib/Drupal/node/NodeAccessController.php
+++ b/core/modules/node/lib/Drupal/node/NodeAccessController.php
@@ -109,11 +109,19 @@ protected function accessGrants(EntityInterface $node, $operation, $langcode = L
     // Check the database for potential access grants.
     $query = db_select('node_access');
     $query->addExpression('1');
+    // Only interested for granting in the current operation.
     $query->condition('grant_' . $operation, 1, '>=');
-    $nids = db_or()->condition('nid', $node->id());
+    // Check for grants for this node and the correct langcode.
+    $nids = db_and()
+      ->condition('nid', $node->nid)
+      ->condition('langcode', $langcode);
+    // If node is published also take the default grant into account, the
+    // default is saved with nid = 0.
     $status = $node instanceof EntityNG ? $node->status : $node->get('status', $langcode)->value;
     if ($status) {
-      $nids->condition('nid', 0);
+      $nids = db_or()
+        ->condition($nids)
+        ->condition('nid', 0);
     }
     $query->condition($nids);
     $query->range(0, 1);
diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageAwareCombinationTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageAwareCombinationTest.php
new file mode 100644
index 0000000..82ecc60
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageAwareCombinationTest.php
@@ -0,0 +1,324 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\node\Tests\NodeAccessLanguageAwareCombinationTest.
+ */
+
+namespace Drupal\node\Tests;
+
+use Drupal\Core\Language\Language;
+
+/**
+ * Tests node access with multiple languages and access control modules.
+ */
+class NodeAccessLanguageAwareCombinationTest extends NodeTestBase {
+
+  /**
+   * Enable language and two node access modules.
+   *
+   * @var array
+   */
+  public static $modules = array('language', 'node_access_test_language', 'node_access_test');
+
+  /**
+   * A set of nodes to use in testing.
+   *
+   * @var array
+   */
+  protected $nodes = array();
+
+  /**
+   * A normal authenticated user.
+   *
+   * @var \Drupal\user\Plugin\Core\Entity\User.
+   */
+  protected $web_user;
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Node access language-aware combination',
+      'description' => 'Tests node access functionality with multiple languages and two node access modules.',
+      'group' => 'Node',
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+
+    // After enabling a node access module, the access table has to be rebuild.
+    node_access_rebuild();
+
+    // Add Hungarian and Catalan.
+    $language = new Language(array(
+      'langcode' => 'hu',
+    ));
+    language_save($language);
+    $language = new Language(array(
+      'langcode' => 'ca',
+    ));
+    language_save($language);
+
+    // Create a normal authenticated user.
+    $this->web_user = $this->drupalCreateUser(array('access content'));
+
+    // Load the user 1 user for later use.
+    $this->admin_user = user_load(1);
+
+    // The node_access_test_language module allows individual translations of
+    // a node to be marked private (not viewable by normal users), and the
+    // node_access_test module allows whole nodes to be marked private. (In a
+    // real-world implementation, hook_node_access_records_alter() might be
+    // implemented by one or both modules to enforce that private nodes or
+    // translations are always private, but we want to test the default,
+    // additive behavior of node access).
+
+    // Create six Hungarian nodes with Catalan translations:
+    // 1. One public with neither language marked as private.
+    // 2. One private with neither language marked as private.
+    // 3. One public with only the Hungarian translation private.
+    // 4. One public with only the Catalan translation private.
+    // 5. One public with both the Hungarian and Catalan translations private.
+    // 6. One private with both the Hungarian and Catalan translations private.
+    $this->nodes['public_both_public'] = $node = $this->drupalCreateNode(array(
+      'body' => array(array()),
+      'langcode' => 'hu',
+      'field_private' => array(array('value' => 0)),
+      'private' => FALSE,
+    ));
+    $translation = $node->getTranslation('ca');
+    $translation->field_private[0]->value = 0;
+    $node->save();
+
+    $this->nodes['private_both_public'] = $node = $this->drupalCreateNode(array(
+      'body' => array(array()),
+      'langcode' => 'hu',
+      'field_private' => array(array('value' => 0)),
+      'private' => TRUE,
+    ));
+    $translation = $node->getTranslation('ca');
+    $translation->field_private[0]->value = 0;
+    $node->save();
+
+    $this->nodes['public_hu_private'] = $node = $this->drupalCreateNode(array(
+      'body' => array(array()),
+      'langcode' => 'hu',
+      'field_private' => array(array('value' => 1)),
+      'private' => FALSE,
+    ));
+    $translation = $node->getTranslation('ca');
+    $translation->field_private[0]->value = 0;
+    $node->save();
+
+    $this->nodes['public_ca_private'] = $node = $this->drupalCreateNode(array(
+      'body' => array(array()),
+      'langcode' => 'hu',
+      'field_private' => array(array('value' => 0)),
+      'private' => FALSE,
+    ));
+    $translation = $node->getTranslation('ca');
+    $translation->field_private[0]->value = 1;
+    $node->save();
+
+    $this->nodes['public_both_private'] = $node = $this->drupalCreateNode(array(
+      'body' => array(array()),
+      'langcode' => 'hu',
+      'field_private' => array(array('value' => 1)),
+      'private' => FALSE,
+    ));
+    $translation = $node->getTranslation('ca');
+    $translation->field_private[0]->value = 1;
+    $node->save();
+
+    $this->nodes['private_both_private'] = $node = $this->drupalCreateNode(array(
+      'body' => array(array()),
+      'langcode' => 'hu',
+      'field_private' => array(array('value' => 1)),
+      'private' => TRUE,
+    ));
+    $translation = $node->getTranslation('ca');
+    $translation->field_private[0]->value = 1;
+    $node->save();
+
+    $this->nodes['public_no_language_private'] = $this->drupalCreateNode(array(
+      'field_private' => array(array('value' => 1)),
+      'private' => FALSE,
+    ));
+    $this->nodes['public_no_language_public'] = $this->drupalCreateNode(array(
+      'field_private' => array(array('value' => 0)),
+      'private' => FALSE,
+    ));
+    $this->nodes['private_no_language_private'] = $this->drupalCreateNode(array(
+      'field_private' => array(array('value' => 1)),
+      'private' => TRUE,
+    ));
+    $this->nodes['private_no_language_public'] = $this->drupalCreateNode(array(
+      'field_private' => array(array('value' => 1)),
+      'private' => TRUE,
+    ));
+  }
+
+  /**
+   * Tests node_access() and node access queries with multiple node languages.
+   */
+  function testNodeAccessLanguageAwareCombination() {
+
+    $expected_node_access = array('view' => TRUE, 'update' => FALSE, 'delete' => FALSE);
+    $expected_node_access_no_access = array('view' => FALSE, 'update' => FALSE, 'delete' => FALSE);
+
+    // When the node and both translations are public, access should only be
+    // denied when a translation that does not exist is requested.
+    $this->assertNodeAccess($expected_node_access, $this->nodes['public_both_public'], $this->web_user);
+    $this->assertNodeAccess($expected_node_access, $this->nodes['public_both_public'], $this->web_user, 'hu');
+    $this->assertNodeAccess($expected_node_access, $this->nodes['public_both_public'], $this->web_user, 'ca');
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_both_public'], $this->web_user, 'en');
+
+    // If the node is marked private but both existing translations are not,
+    // access should still be granted, because the grants are additive.
+    $this->assertNodeAccess($expected_node_access, $this->nodes['private_both_public'], $this->web_user);
+    $this->assertNodeAccess($expected_node_access, $this->nodes['private_both_public'], $this->web_user, 'hu');
+    $this->assertNodeAccess($expected_node_access, $this->nodes['private_both_public'], $this->web_user, 'ca');
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_both_public'], $this->web_user, 'en');
+
+    // If the node is marked private, but a existing translation is public,
+    // access should only be granted for the public translation.
+    // For a translation that does not exist yet (English translation),
+    // the access is denied.
+    // With the Hungarian translation marked as private, but the Catalan
+    // translation public, the access is granted.
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_hu_private'], $this->web_user);
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_hu_private'], $this->web_user, 'hu');
+    $this->assertNodeAccess($expected_node_access, $this->nodes['public_hu_private'], $this->web_user, 'ca');
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_hu_private'], $this->web_user, 'en');
+
+    // With the Catalan translation marked as private, but the node public,
+    // access is granted for the existing Hungarian translation, but not for
+    // the Catalan nor the English ones.
+    $this->assertNodeAccess($expected_node_access, $this->nodes['public_ca_private'], $this->web_user);
+    $this->assertNodeAccess($expected_node_access, $this->nodes['public_ca_private'], $this->web_user, 'hu');
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_ca_private'], $this->web_user, 'ca');
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_ca_private'], $this->web_user, 'en');
+
+    // With both translations marked as private, but the node public,
+    // access should be denied in all cases.
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_both_private'], $this->web_user);
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_both_private'], $this->web_user, 'hu');
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_both_private'], $this->web_user, 'ca');
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_both_private'], $this->web_user, 'en');
+
+    // If the node and both its existing translations are private, access
+    // should be denied in all cases.
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_both_private'], $this->web_user);
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_both_private'], $this->web_user, 'hu');
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_both_private'], $this->web_user, 'ca');
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_both_private'], $this->web_user, 'en');
+
+    // No access for all languages as the language aware node access module
+    // denies access.
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_no_language_private'], $this->web_user);
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_no_language_private'], $this->web_user, 'hu');
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_no_language_private'], $this->web_user, 'ca');
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_no_language_private'], $this->web_user, 'en');
+
+    // Access only for request with no language defined.
+    $this->assertNodeAccess($expected_node_access, $this->nodes['public_no_language_public'], $this->web_user);
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_no_language_public'], $this->web_user, 'hu');
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_no_language_public'], $this->web_user, 'ca');
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['public_no_language_public'], $this->web_user, 'en');
+
+    // No access for all languages as both node access modules deny access.
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_no_language_private'], $this->web_user);
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_no_language_private'], $this->web_user, 'hu');
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_no_language_private'], $this->web_user, 'ca');
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_no_language_private'], $this->web_user, 'en');
+
+    // No access for all languages as the non language aware node access module
+    // denies access.
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_no_language_public'], $this->web_user);
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_no_language_public'], $this->web_user, 'hu');
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_no_language_public'], $this->web_user, 'ca');
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['private_no_language_public'], $this->web_user, 'en');
+
+
+    // Query the node table with the node access tag in several languages.
+
+    // Query with no language specified. The fallback (hu or und) will be used.
+    $select = db_select('node', 'n')
+    ->fields('n', array('nid'))
+    ->addMetaData('account', $this->web_user)
+    ->addTag('node_access');
+    $nids = $select->execute()->fetchAllAssoc('nid');
+
+    // Four nodes should be returned with public Hungarian translations or the
+    // no language public node.
+    $this->assertEqual(count($nids), 4, 'db_select() returns 4 nodes when no langcode is specified.');
+    $this->assertTrue(array_key_exists($this->nodes['public_both_public']->nid, $nids), 'Returned node ID is full public node.');
+    $this->assertTrue(array_key_exists($this->nodes['public_ca_private']->nid, $nids), 'Returned node ID is Hungarian public only node.');
+    $this->assertTrue(array_key_exists($this->nodes['private_both_public']->nid, $nids), 'Returned node ID is both public non-language-aware private only node.');
+    $this->assertTrue(array_key_exists($this->nodes['public_no_language_public']->nid, $nids), 'Returned node ID is no language public node.');
+
+    // Query with Hungarian (hu) specified.
+    $select = db_select('node', 'n')
+    ->fields('n', array('nid'))
+    ->addMetaData('account', $this->web_user)
+    ->addMetaData('langcode', 'hu')
+    ->addTag('node_access');
+    $nids = $select->execute()->fetchAllAssoc('nid');
+
+    // Three nodes should be returned (with public Hungarian translations).
+    $this->assertEqual(count($nids), 3, 'db_select() returns 3 nodes.');
+    $this->assertTrue(array_key_exists($this->nodes['public_both_public']->nid, $nids), 'Returned node ID is both public node.');
+    $this->assertTrue(array_key_exists($this->nodes['public_ca_private']->nid, $nids), 'Returned node ID is Hungarian public only node.');
+    $this->assertTrue(array_key_exists($this->nodes['private_both_public']->nid, $nids), 'Returned node ID is both public non-language-aware private only node.');
+
+    // Query with Catalan (ca) specified.
+    $select = db_select('node', 'n')
+    ->fields('n', array('nid'))
+    ->addMetaData('account', $this->web_user)
+    ->addMetaData('langcode', 'ca')
+    ->addTag('node_access');
+    $nids = $select->execute()->fetchAllAssoc('nid');
+
+    // Three nodes should be returned (with public Catalan translations).
+    $this->assertEqual(count($nids), 3, 'db_select() returns 3 nodes.');
+    $this->assertTrue(array_key_exists($this->nodes['public_both_public']->nid, $nids), 'Returned node ID is both public node.');
+    $this->assertTrue(array_key_exists($this->nodes['public_hu_private']->nid, $nids), 'Returned node ID is Catalan public only node.');
+    $this->assertTrue(array_key_exists($this->nodes['private_both_public']->nid, $nids), 'Returned node ID is both public non-language-aware private only node.');
+
+    // Query with German (de) specified.
+    $select = db_select('node', 'n')
+    ->fields('n', array('nid'))
+    ->addMetaData('account', $this->web_user)
+    ->addMetaData('langcode', 'de')
+    ->addTag('node_access');
+    $nids = $select->execute()->fetchAllAssoc('nid');
+
+    // There are no nodes with German translations, so no results are returned.
+    $this->assertTrue(empty($nids), 'db_select() returns an empty result.');
+
+    // Query the nodes table as admin user (full access) with the node
+    // access tag and no specific langcode.
+    $select = db_select('node', 'n')
+    ->fields('n', array('nid'))
+    ->addMetaData('account', $this->admin_user)
+    ->addTag('node_access');
+    $nids = $select->execute()->fetchAllAssoc('nid');
+
+    // All nodes are returned.
+    $this->assertEqual(count($nids), 10, 'db_select() returns all nodes.');
+
+    // Query the nodes table as admin user (full access) with the node
+    // access tag and langcode de.
+    $select = db_select('node', 'n')
+    ->fields('n', array('nid'))
+    ->addMetaData('account', $this->admin_user)
+    ->addMetaData('langcode', 'de')
+    ->addTag('node_access');
+    $nids = $select->execute()->fetchAllAssoc('nid');
+
+    // Even though there is no German translation, all nodes are returned
+    // because node access filtering does not occr when the user is user 1.
+    $this->assertEqual(count($nids), 10, 'db_select() returns all nodes.');
+  }
+
+}
diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageAwareTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageAwareTest.php
new file mode 100644
index 0000000..e522c9f
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageAwareTest.php
@@ -0,0 +1,271 @@
+<?php
+
+/**
+ * @file
+ * Contains Drupal\node\Tests\NodeAccessLanguageAwareTest.
+ */
+
+namespace Drupal\node\Tests;
+
+use Drupal\Core\Language\Language;
+
+/**
+ * Tests node access functionality for multiple languages.
+ */
+class NodeAccessLanguageAwareTest extends NodeTestBase {
+
+  /**
+   * Enable language and a language-aware node access module.
+   *
+   * @var array
+   */
+  public static $modules = array('language', 'node_access_test_language');
+
+  /**
+   * A set of nodes to use in testing.
+   *
+   * @var array
+   */
+  protected $nodes = array();
+
+  /**
+   * A normal authenticated user.
+   *
+   * @var \Drupal\user\Plugin\Core\Entity\User.
+   */
+  protected $web_user;
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Node access language-aware',
+      'description' => 'Test node_access and db_select() with node_access tag functionality with multiple languages with node_access_test_language which is language-aware.',
+      'group' => 'Node',
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+
+    // After enabling a node access module, the access table has to be rebuild.
+    node_access_rebuild();
+
+    // Create a normal authenticated user.
+    $this->web_user = $this->drupalCreateUser(array('access content'));
+
+    // Load the user 1 user for later use.
+    $this->admin_user = user_load(1);
+
+    // Add Hungarian and Catalan.
+    $language = new Language(array(
+      'langcode' => 'hu',
+    ));
+    language_save($language);
+    $language = new Language(array(
+      'langcode' => 'ca',
+    ));
+    language_save($language);
+
+    // The node_access_test_language module allows individual translations of
+    // a node to be marked private (not viewable by normal users).
+
+    // Create six nodes:
+    // 1. Four Hungarian nodes with Catalan translations
+    //   - One with neither language marked as private.
+    //   - One with only the Hungarian translation private.
+    //   - One with only the Catalan translation private.
+    //   - One with both the Hungarian and Catalan translations private.
+    // 2. Two nodes with no language specified.
+    //   - One public.
+    //   - One private.
+    $this->nodes['both_public'] = $node = $this->drupalCreateNode(array(
+      'body' => array(array()),
+      'langcode' => 'hu',
+      'field_private' => array(array('value' => 0)),
+    ));
+    $translation = $node->getTranslation('ca');
+    $translation->field_private[0]->value = 0;
+    $node->save();
+
+    $this->nodes['ca_private'] = $node = $this->drupalCreateNode(array(
+      'body' => array(array()),
+      'langcode' => 'hu',
+      'field_private' => array(array('value' => 0)),
+    ));
+    $translation = $node->getTranslation('ca');
+    $translation->field_private[0]->value = 1;
+    $node->save();
+
+    $this->nodes['hu_private'] = $node = $this->drupalCreateNode(array(
+      'body' => array(array()),
+      'langcode' => 'hu',
+      'field_private' => array(array('value' => 1)),
+    ));
+    $translation = $node->getTranslation('ca');
+    $translation->field_private[0]->value = 0;
+    $node->save();
+
+    $this->nodes['both_private'] = $node = $this->drupalCreateNode(array(
+      'body' => array(array()),
+      'langcode' => 'hu',
+      'field_private' => array(array('value' => 1)),
+    ));
+    $translation = $node->getTranslation('ca');
+    $translation->field_private[0]->value = 1;
+    $node->save();
+
+    $this->nodes['no_language_public'] = $this->drupalCreateNode(array(
+      'field_private' => array(array('value' => 0)),
+    ));
+    $this->nodes['no_language_private'] = $this->drupalCreateNode(array(
+      'field_private' => array(array('value' => 1)),
+    ));
+  }
+
+  /**
+   * Tests node_access() and node access queries with multiple node languages.
+   */
+  function testNodeAccessLanguageAware() {
+    // The node_access_test_language module only grants view access.
+    $expected_node_access = array('view' => TRUE, 'update' => FALSE, 'delete' => FALSE);
+    $expected_node_access_no_access = array('view' => FALSE, 'update' => FALSE, 'delete' => FALSE);
+
+    // When both Hungarian and Catalan are marked as public:
+    // Access to the Hungarian translation should be granted when no language
+    // is specified or when the Hungarian translation is specified explicitly.
+    $this->assertNodeAccess($expected_node_access, $this->nodes['both_public'], $this->web_user);
+    $this->assertNodeAccess($expected_node_access, $this->nodes['both_public'], $this->web_user, 'hu');
+    // Access to the Catalan translation should also be granted.
+    $this->assertNodeAccess($expected_node_access, $this->nodes['both_public'], $this->web_user, 'ca');
+    // There is no English translation, so a request to access the English
+    // translation is denied.
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['both_public'], $this->web_user, 'en');
+
+    // When Hungarian is marked as private:
+    // Access to the Hungarian translation should be denied when no language
+    // is specified or when the Hungarian translation is specified explicitly.
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['hu_private'], $this->web_user);
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['hu_private'], $this->web_user, 'hu');
+    // Access to the Catalan translation should be granted.
+    $this->assertNodeAccess($expected_node_access, $this->nodes['hu_private'], $this->web_user, 'ca');
+    // There is no English translation, so a request to access the English
+    // translation is denied.
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['hu_private'], $this->web_user, 'en');
+
+    // When Catalan is marked as private:
+    // Access to the Hungarian translation should be granted when no language
+    // is specified or when the Hungarian translation is specified explicitly.
+    $this->assertNodeAccess($expected_node_access, $this->nodes['ca_private'], $this->web_user);
+    $this->assertNodeAccess($expected_node_access, $this->nodes['ca_private'], $this->web_user, 'hu');
+    // Access to the Catalan translation should be granted.
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['ca_private'], $this->web_user, 'ca');
+    // There is no English translation, so a request to access the English
+    // translation is denied.
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['ca_private'], $this->web_user, 'en');
+
+    // When both translations are marked as private, access should be denied
+    // regardless of the language specified.
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['both_private'], $this->web_user);
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['both_private'], $this->web_user, 'hu');
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['both_private'], $this->web_user, 'ca');
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['both_private'], $this->web_user, 'en');
+
+    // No language private node:
+    // The only existing language (not specified) is set as private, so no
+    // access on every language.
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['no_language_private'], $this->web_user);
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['no_language_private'], $this->web_user, 'hu');
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['no_language_private'], $this->web_user, 'ca');
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['no_language_private'], $this->web_user, 'en');
+
+    // No language public node:
+    // The only existing language (not specified) is set as public, so only
+    // the request with no language will give access, as this request will be
+    // made with the lancode of the node, which is "not specified".
+    $this->assertNodeAccess($expected_node_access, $this->nodes['no_language_public'], $this->web_user);
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['no_language_public'], $this->web_user, 'hu');
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['no_language_public'], $this->web_user, 'ca');
+    $this->assertNodeAccess($expected_node_access_no_access, $this->nodes['no_language_public'], $this->web_user, 'en');
+
+    // Query the node table with the node access tag in several languages.
+
+    // Query with no language specified. The fallback (hu) will be used.
+    $select = db_select('node', 'n')
+    ->fields('n', array('nid'))
+    ->addMetaData('account', $this->web_user)
+    ->addTag('node_access');
+    $nids = $select->execute()->fetchAllAssoc('nid');
+
+    // Three nodes should be returned:
+    // - Node with both translations public.
+    // - Node with only the Catalan translation marked as private.
+    // - No language node marked as public.
+    $this->assertEqual(count($nids), 3, 'db_select() returns 3 nodes when no langcode is specified.');
+    $this->assertTrue(array_key_exists($this->nodes['both_public']->nid, $nids), 'The node with both translations public is returned.');
+    $this->assertTrue(array_key_exists($this->nodes['ca_private']->nid, $nids), 'The node with only the Catalan translation private is returned.');
+    $this->assertTrue(array_key_exists($this->nodes['no_language_public']->nid, $nids), 'The node with no language is returned.');
+
+    // Query with Hungarian (hu) specified.
+    $select = db_select('node', 'n')
+    ->fields('n', array('nid'))
+    ->addMetaData('account', $this->web_user)
+    ->addMetaData('langcode', 'hu')
+    ->addTag('node_access');
+    $nids = $select->execute()->fetchAllAssoc('nid');
+
+    // Two nodes should be returned: the node with both translations public,
+    // and the node with only the Catalan translation marked as private.
+    $this->assertEqual(count($nids), 2, 'db_select() returns 2 nodes when the hu langcode is specified.');
+    $this->assertTrue(array_key_exists($this->nodes['both_public']->nid, $nids), 'The node with both translations public is returned.');
+    $this->assertTrue(array_key_exists($this->nodes['ca_private']->nid, $nids), 'The node with only the Catalan translation private is returned.');
+
+    // Query with Catalan (ca) specified.
+    $select = db_select('node', 'n')
+    ->fields('n', array('nid'))
+    ->addMetaData('account', $this->web_user)
+    ->addMetaData('langcode', 'ca')
+    ->addTag('node_access');
+    $nids = $select->execute()->fetchAllAssoc('nid');
+
+    // Two nodes should be returned: the node with both translations public,
+    // and the node with only the Hungarian translation marked as private.
+    $this->assertEqual(count($nids), 2, 'db_select() returns 2 nodes when the hu langcode is specified.');
+    $this->assertTrue(array_key_exists($this->nodes['both_public']->nid, $nids), 'The node with both translations public is returned.');
+    $this->assertTrue(array_key_exists($this->nodes['hu_private']->nid, $nids), 'The node with only the Hungarian translation private is returned.');
+
+    // Query with German (de) specified.
+    $select = db_select('node', 'n')
+    ->fields('n', array('nid'))
+    ->addMetaData('account', $this->web_user)
+    ->addMetaData('langcode', 'de')
+    ->addTag('node_access');
+    $nids = $select->execute()->fetchAllAssoc('nid');
+
+    // There are no nodes with German translations, so no results are returned.
+    $this->assertTrue(empty($nids), 'db_select() returns an empty result when the de langcode is specified.');
+
+    // Query the nodes table as admin user (full access) with the node
+    // access tag and no specific langcode.
+    $select = db_select('node', 'n')
+    ->fields('n', array('nid'))
+    ->addMetaData('account', $this->admin_user)
+    ->addTag('node_access');
+    $nids = $select->execute()->fetchAllAssoc('nid');
+
+    // All nodes are returned.
+    $this->assertEqual(count($nids), 6, 'db_select() returns all nodes.');
+
+    // Query the nodes table as admin user (full access) with the node
+    // access tag and langcode de.
+    $select = db_select('node', 'n')
+    ->fields('n', array('nid'))
+    ->addMetaData('account', $this->admin_user)
+    ->addMetaData('langcode', 'de')
+    ->addTag('node_access');
+    $nids = $select->execute()->fetchAllAssoc('nid');
+
+    // Even though there is no German translation, all nodes are returned
+    // because node access filtering does not occr when the user is user 1.
+    $this->assertEqual(count($nids), 6, 'db_select() returns all nodes.');
+  }
+
+}
diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageTest.php
index b418ef4..85b0a52 100644
--- a/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeAccessLanguageTest.php
@@ -24,34 +24,20 @@ class NodeAccessLanguageTest extends NodeTestBase {
   public static function getInfo() {
     return array(
       'name' => 'Node access language',
-      'description' => 'Test node_access functionality with multiple languages.',
+      'description' => 'Test node_access and db_select() with node_access tag functionality with multiple languages with a test node access module that is not language-aware.',
       'group' => 'Node',
     );
   }
 
-  /**
-   * Asserts node_access correctly grants or denies access.
-   */
-  function assertNodeAccess($ops, $node, $account, $langcode = NULL) {
-    foreach ($ops as $op => $result) {
-      $msg = t("node_access returns @result with operation '@op', language code @langcode.", array('@result' => $result ? 'true' : 'false', '@op' => $op, '@langcode' => !empty($langcode) ? "'$langcode'" : 'empty'));
-      $this->assertEqual($result, node_access($op, $node, $account, $langcode), $msg);
-    }
-  }
-
   function setUp() {
     parent::setUp();
 
-    // Clear permissions for authenticated users.
-    db_delete('role_permission')
-      ->condition('rid', DRUPAL_AUTHENTICATED_RID)
-      ->execute();
-  }
+    // After enabling a node access module, the access table has to be rebuild.
+    node_access_rebuild();
+
+    // Enable the private node feature of the node_access_test module.
+    state()->set('node_access_test.private', TRUE);
 
-  /**
-   * Runs tests for node_access function with multiple languages.
-   */
-  function testNodeAccess() {
     // Add Hungarian and Catalan.
     $language = new Language(array(
       'langcode' => 'hu',
@@ -61,31 +47,198 @@ function testNodeAccess() {
       'langcode' => 'ca',
     ));
     language_save($language);
+  }
+
+  /**
+   * Tests node_access() with multiple node languages and no private nodes.
+   */
+  function testNodeAccess() {
+    $web_user = $this->drupalCreateUser(array('access content'));
+
+    $expected_node_access = array('view' => TRUE, 'update' => FALSE, 'delete' => FALSE);
+    $expected_node_access_no_access = array('view' => FALSE, 'update' => FALSE, 'delete' => FALSE);
+
+    // Creating a public node with langcode Hungarian, will be saved as
+    // the fallback in node access table.
+    $node_public = $this->drupalCreateNode(array('body' => array(array()), 'langcode' => 'hu', 'private' => FALSE));
+    $this->assertTrue($node_public->langcode == 'hu', 'Node created as Hungarian.');
+
+    // Tests the default access is provided for the public Hungarian node.
+    $this->assertNodeAccess($expected_node_access, $node_public, $web_user);
+
+    // Tests that Hungarian provided specifically results in the same.
+    $this->assertNodeAccess($expected_node_access, $node_public, $web_user, 'hu');
+
+    // There is no specific Catalan version of this node and Croatian is not
+    // even set up on the system in this scenario, so the user will not get
+    // access to these nodes.
+    $this->assertNodeAccess($expected_node_access_no_access, $node_public, $web_user, 'ca');
+    $this->assertNodeAccess($expected_node_access_no_access, $node_public, $web_user, 'hr');
+
+    // Creating a public node with no special langcode, like when no language
+    // module enabled.
+    $node_public_no_language = $this->drupalCreateNode(array('private' => FALSE));
+    $this->assertTrue($node_public_no_language->langcode == LANGUAGE_NOT_SPECIFIED, 'Node created with not specified language.');
+
+    // Tests that access is granted if requested with no language.
+    $this->assertNodeAccess($expected_node_access, $node_public_no_language, $web_user);
+
+    // Tests that access is not granted if requested with Hungarian language.
+    $this->assertNodeAccess($expected_node_access_no_access, $node_public_no_language, $web_user, 'hu');
 
-    // Tests the default access provided for a published Hungarian node.
+    // There is no specific Catalan version of this node and Croatian is not
+    // even set up on the system in this scenario, so the user will not get
+    // access to these nodes.
+    $this->assertNodeAccess($expected_node_access_no_access, $node_public_no_language, $web_user, 'ca');
+    $this->assertNodeAccess($expected_node_access_no_access, $node_public_no_language, $web_user, 'hr');
+
+    // Reset the node access cache and turn on our test node_access() code.
+    drupal_static_reset('node_access');
+    variable_set('node_access_test_secret_catalan', 1);
+
+    // Tests that access is granted if requested with no language.
+    $this->assertNodeAccess($expected_node_access, $node_public_no_language, $web_user);
+
+    // Tests that Hungarian is still not accessible.
+    $this->assertNodeAccess($expected_node_access_no_access, $node_public_no_language, $web_user, 'hu');
+
+    // Tests that Catalan is still not accessible.
+    $this->assertNodeAccess($expected_node_access_no_access, $node_public_no_language, $web_user, 'ca');
+  }
+
+  /**
+   * Tests node_access() with multiple node languages and private nodes.
+   */
+  function testNodeAccessPrivate() {
     $web_user = $this->drupalCreateUser(array('access content'));
+
     $node = $this->drupalCreateNode(array('body' => array(array()), 'langcode' => 'hu'));
     $this->assertTrue($node->langcode == 'hu', 'Node created as Hungarian.');
     $expected_node_access = array('view' => TRUE, 'update' => FALSE, 'delete' => FALSE);
-    $this->assertNodeAccess($expected_node_access, $node, $web_user);
+    $expected_node_access_no_access = array('view' => FALSE, 'update' => FALSE, 'delete' => FALSE);
+
+    // Creating a private node with langcode Hungarian, will be saved as
+    // the fallback in node access table.
+    $node_public = $this->drupalCreateNode(array('body' => array(array()), 'langcode' => 'hu', 'private' => TRUE));
+    $this->assertTrue($node_public->langcode == 'hu', 'Node created as Hungarian.');
+
+    // Tests the default access is not provided for the private Hungarian node.
+    $this->assertNodeAccess($expected_node_access_no_access, $node_public, $web_user);
 
     // Tests that Hungarian provided specifically results in the same.
-    $this->assertNodeAccess($expected_node_access, $node, $web_user, 'hu');
+    $this->assertNodeAccess($expected_node_access_no_access, $node_public, $web_user, 'hu');
 
     // There is no specific Catalan version of this node and Croatian is not
-    // even set up on the system in this scenario, so these languages will not
-    // play a role in the node's permissions.
-    $this->assertNodeAccess($expected_node_access, $node, $web_user, 'ca');
-    $this->assertNodeAccess($expected_node_access, $node, $web_user, 'hr');
+    // even set up on the system in this scenario, so the user will not get
+    // access to these nodes.
+    $this->assertNodeAccess($expected_node_access_no_access, $node_public, $web_user, 'ca');
+    $this->assertNodeAccess($expected_node_access_no_access, $node_public, $web_user, 'hr');
+
+    // Creating a private node with no special langcode, like when no language
+    // module enabled.
+    $node_private_no_language = $this->drupalCreateNode(array('private' => TRUE));
+    $this->assertTrue($node_private_no_language->langcode == LANGUAGE_NOT_SPECIFIED, 'Node created with not specified language.');
+
+    // Tests that access is not granted if requested with no language.
+    $this->assertNodeAccess($expected_node_access_no_access, $node_private_no_language, $web_user);
+
+    // Tests that access is not granted if requested with Hungarian language.
+    $this->assertNodeAccess($expected_node_access_no_access, $node_private_no_language, $web_user, 'hu');
+
+    // There is no specific Catalan version of this node and Croatian is not
+    // even set up on the system in this scenario, so the user will not get
+    // access to these nodes.
+    $this->assertNodeAccess($expected_node_access_no_access, $node_private_no_language, $web_user, 'ca');
+    $this->assertNodeAccess($expected_node_access_no_access, $node_private_no_language, $web_user, 'hr');
 
     // Reset the node access cache and turn on our test node_access() code.
     entity_access_controller('node')->resetCache();
     state()->set('node_access_test_secret_catalan', 1);
 
-    // Tests that Hungarian is still accessible.
-    $this->assertNodeAccess($expected_node_access, $node, $web_user, 'hu');
+    // Tests that access is not granted if requested with no language.
+    $this->assertNodeAccess($expected_node_access_no_access, $node_private_no_language, $web_user);
+
+    // Tests that Hungarian is still not accessible.
+    $this->assertNodeAccess($expected_node_access_no_access, $node_private_no_language, $web_user, 'hu');
 
-    // Tests that Catalan is not accessible anymore.
-    $this->assertNodeAccess(array('view' => FALSE, 'update' => FALSE, 'delete' => FALSE), $node, $web_user, 'ca');
+    // Tests that Catalan is still not accessible.
+    $this->assertNodeAccess($expected_node_access_no_access, $node_private_no_language, $web_user, 'ca');
   }
+
+  /**
+   * Tests db_select() with a 'node_access' tag and langcode metadata.
+   */
+  function testNodeAccessQueryTag() {
+    // Create a normal authenticated user.
+    $web_user = $this->drupalCreateUser(array('access content'));
+
+    // Load the user 1 user for later use.
+    $admin_user = user_load(1);
+
+    // Creating a private node with langcode Hungarian, will be saved as
+    // the fallback in node access table.
+    $node_private = $this->drupalCreateNode(array('body' => array(array()), 'langcode' => 'hu', 'private' => TRUE));
+    $this->assertTrue($node_private->langcode == 'hu', 'Node created as Hungarian.');
+
+    // Creating a public node with langcode Hungarian, will be saved as
+    // the fallback in node access table.
+    $node_public = $this->drupalCreateNode(array('body' => array(array()), 'langcode' => 'hu', 'private' => FALSE));
+    $this->assertTrue($node_public->langcode == 'hu', 'Node created as Hungarian.');
+
+    // Creating a public node with no special langcode, like when no language
+    // module enabled.
+    $node_no_language = $this->drupalCreateNode(array('private' => FALSE));
+    $this->assertTrue($node_no_language->langcode == LANGUAGE_NOT_SPECIFIED, 'Node created with not specified language.');
+
+    // Query the nodes table as the web user with the node access tag and no
+    // specific langcode.
+    $select = db_select('node', 'n')
+    ->fields('n', array('nid'))
+    ->addMetaData('account', $web_user)
+    ->addTag('node_access');
+    $nids = $select->execute()->fetchAllAssoc('nid');
+
+    // The public node and no language node should be returned. Because no
+    // langcode is given it will use the fallback node.
+    $this->assertEqual(count($nids), 2, 'db_select() returns 2 node');
+    $this->assertTrue(array_key_exists($node_public->nid, $nids), 'Returned node ID is public node.');
+    $this->assertTrue(array_key_exists($node_no_language->nid, $nids), 'Returned node ID is no language node.');
+
+    // Query the nodes table as the web user with the node access tag and
+    // langcode de.
+    $select = db_select('node', 'n')
+    ->fields('n', array('nid'))
+    ->addMetaData('account', $web_user)
+    ->addMetaData('langcode', 'de')
+    ->addTag('node_access');
+    $nids = $select->execute()->fetchAllAssoc('nid');
+
+    // Because no nodes are created in German, no nodes are returned.
+    $this->assertTrue(empty($nids), 'db_select() returns an empty result.');
+
+    // Query the nodes table as admin user (full access) with the node
+    // access tag and no specific langcode.
+    $select = db_select('node', 'n')
+    ->fields('n', array('nid'))
+    ->addMetaData('account', $admin_user)
+    ->addTag('node_access');
+    $nids = $select->execute()->fetchAllAssoc('nid');
+
+    // All nodes are returned.
+    $this->assertEqual(count($nids), 3, 'db_select() returns all three nodes.');
+
+    // Query the nodes table as admin user (full access) with the node
+    // access tag and langcode de.
+    $select = db_select('node', 'n')
+    ->fields('n', array('nid'))
+    ->addMetaData('account', $admin_user)
+    ->addMetaData('langcode', 'de')
+    ->addTag('node_access');
+    $nids = $select->execute()->fetchAllAssoc('nid');
+
+    // All nodes are returned because node access tag is not invoked when
+    // the user is user 1.
+    $this->assertEqual(count($nids), 3, 'db_select() returns all three nodes.');
+  }
+
 }
diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeAccessTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeAccessTest.php
index 4f87348..73b3a44 100644
--- a/core/modules/node/lib/Drupal/node/Tests/NodeAccessTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeAccessTest.php
@@ -23,16 +23,6 @@ public static function getInfo() {
     );
   }
 
-  /**
-   * Asserts node_access() correctly grants or denies access.
-   */
-  function assertNodeAccess($ops, $node, $account) {
-    foreach ($ops as $op => $result) {
-      $msg = t("node_access returns @result with operation '@op'.", array('@result' => $result ? 'true' : 'false', '@op' => $op));
-      $this->assertEqual($result, node_access($op, $node, $account), $msg);
-    }
-  }
-
   function setUp() {
     parent::setUp();
     // Clear permissions for authenticated users.
@@ -76,4 +66,5 @@ function testNodeAccess() {
     $node5 = $this->drupalCreateNode();
     $this->assertNodeAccess(array('view' => TRUE, 'update' => FALSE, 'delete' => FALSE), $node5, $web_user3);
   }
+
 }
diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeTestBase.php b/core/modules/node/lib/Drupal/node/Tests/NodeTestBase.php
index f1ed6aa..c5b21e2 100644
--- a/core/modules/node/lib/Drupal/node/Tests/NodeTestBase.php
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeTestBase.php
@@ -30,4 +30,35 @@ function setUp() {
       $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article'));
     }
   }
+
+  /**
+   * Asserts that node_access() correctly grants or denies access.
+   *
+   * @param array $ops
+   *   An associative array of the expected node access grants for the node
+   *   and account, with each key as the name of an operation (e.g. 'view',
+   *  'delete') and each value a Boolean indicating whether access to that
+   *   operation should be granted.
+   * @param \Drupal\node\Plugin\Core\Entity\Node $node
+   *   The node object to check.
+   * @param \Drupal\user\Plugin\Core\Entity\User $account
+   *   The user account for which to check access.
+   * @param string|null $langcode
+   *   (optional) The language code indicating which translation of the node
+   *   to check. If NULL, the untranslated (fallback) access is checked.
+   */
+  function assertNodeAccess(array $ops, $node, $account, $langcode = NULL) {
+    foreach ($ops as $op => $result) {
+      $msg = format_string(
+        'node_access() returns @result with operation %op, language code %langcode.',
+        array(
+          '@result' => $result ? 'true' : 'false',
+          '%op' => $op,
+          '%langcode' => !empty($langcode) ? $langcode : 'empty'
+        )
+      );
+      $this->assertEqual($result, node_access($op, $node, $account, $langcode), $msg);
+    }
+  }
+
 }
diff --git a/core/modules/node/node.api.php b/core/modules/node/node.api.php
index d5e396c..49d5dd1 100644
--- a/core/modules/node/node.api.php
+++ b/core/modules/node/node.api.php
@@ -234,11 +234,18 @@ function hook_node_grants($account, $op) {
  *   of this gid within this realm can edit this node.
  * - 'grant_delete': If set to 1 a user that has been identified as a member
  *   of this gid within this realm can delete this node.
- *
- *
- * When an implementation is interested in a node but want to deny access to
- * everyone, it may return a "deny all" grant:
- *
+ * - langcode: (optional) The language code of a specific translation of the
+ *   node, if any. Modules may add this key to grant different access to
+ *   different translations of a node, such that (e.g.) a particular group
+ *   is granted access to edit the Catalan version of the node, but not the
+ *   Hungarian version. If no value is provided, the langcode is set
+ *   set automatically from the $node parameter and the node's original
+ *   language (if specified) is used as a fallback. Only specify multiple
+ *   grant records with different languages for a node if the site has those
+ *   languages configured.
+ *
+ * A "deny all" grant may be used to deny all access to a particular node or
+ * node translation:
  * @code
  * $grants[] = array(
  *   'realm' => 'all',
@@ -246,15 +253,14 @@ function hook_node_grants($account, $op) {
  *   'grant_view' => 0,
  *   'grant_update' => 0,
  *   'grant_delete' => 0,
- *   'priority' => 1,
+ *   'langcode' => 'ca',
  * );
  * @endcode
- *
- * Setting the priority should cancel out other grants. In the case of a
- * conflict between modules, it is safer to use hook_node_access_records_alter()
- * to return only the deny grant.
- *
- * Note: a deny all grant is not written to the database; denies are implicit.
+ * Note that another module node access module could override this by granting
+ * access to one or more nodes, since grants are additive. To enforce that
+ * access is denied in a particular case, use hook_node_access_records_alter().
+ * Also node that a deny all grant is not written to the database; denies are
+ * implicit.
  *
  * @param \Drupal\Core\Entity\EntityInterface $node
  *   The node that has just been saved.
@@ -271,8 +277,9 @@ function hook_node_access_records(\Drupal\Core\Entity\EntityInterface $node) {
   // treated just like any other node and we completely ignore it.
   if ($node->private) {
     $grants = array();
-    // Only published nodes should be viewable to all users. If we allow access
-    // blindly here, then all users could view an unpublished node.
+    // Only published Catalan translations of private nodes should be viewable
+    // to all users. If we fail to check $node->status, all users would be able
+    // to view an unpublished node.
     if ($node->status) {
       $grants[] = array(
         'realm' => 'example',
@@ -280,6 +287,7 @@ function hook_node_access_records(\Drupal\Core\Entity\EntityInterface $node) {
         'grant_view' => 1,
         'grant_update' => 0,
         'grant_delete' => 0,
+        'langcode' => 'ca'
       );
     }
     // For the example_author array, the GID is equivalent to a UID, which
@@ -292,6 +300,7 @@ function hook_node_access_records(\Drupal\Core\Entity\EntityInterface $node) {
       'grant_view' => 1,
       'grant_update' => 1,
       'grant_delete' => 1,
+      'langcode' => 'ca'
     );
 
     return $grants;
diff --git a/core/modules/node/node.install b/core/modules/node/node.install
index ebbce13..238f375 100644
--- a/core/modules/node/node.install
+++ b/core/modules/node/node.install
@@ -149,6 +149,20 @@ function node_schema() {
         'not null' => TRUE,
         'default' => 0,
       ),
+      'langcode' => array(
+        'description' => 'The {language}.langcode of this node.',
+        'type' => 'varchar',
+        'length' => 12,
+        'not null' => TRUE,
+        'default' => '',
+      ),
+      'fallback' => array(
+        'description' => 'Boolean indicating whether this record should be used as a fallback if a language condition is not provided.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 1,
+      ),
       'gid' => array(
         'description' => "The grant ID a user must possess in the specified realm to gain this row's privileges on the node.",
         'type' => 'int',
@@ -188,7 +202,7 @@ function node_schema() {
         'size' => 'tiny',
       ),
     ),
-    'primary key' => array('nid', 'gid', 'realm'),
+    'primary key' => array('nid', 'gid', 'realm', 'langcode'),
     'foreign keys' => array(
       'affected_node' => array(
         'table' => 'node',
@@ -710,6 +724,34 @@ function node_update_8014() {
 }
 
 /**
+ * Add language support to the {node_access} table
+ */
+function node_update_8015() {
+  // Add the langcode field.
+  $langcode_field = array(
+    'type' => 'varchar',
+    'length' => 12,
+    'not null' => TRUE,
+    'default' => '',
+    'description' => 'The {language}.langcode of this node.',
+  );
+  db_add_field('node_access', 'langcode', $langcode_field);
+
+  // Add the fallback field.
+  $fallback_field = array(
+    'description' => 'Boolean indicating whether this record should be used as a fallback if a language condition is not provided.',
+    'type' => 'int',
+    'unsigned' => TRUE,
+    'not null' => TRUE,
+    'default' => 1,
+  );
+  db_add_field('node_access', 'fallback', $fallback_field);
+
+  db_drop_primary_key('node_access');
+  db_add_primary_key('node_access', array('nid', 'gid', 'realm', 'langcode'));
+}
+
+/**
  * @} End of "addtogroup updates-7.x-to-8.x"
  * The next series of updates should start at 9000.
  */
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index 4c7392b..ef55586 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -2508,10 +2508,6 @@ function node_form_system_themes_admin_form_submit($form, &$form_state) {
  *   TRUE if the operation may be performed, FALSE otherwise.
  *
  * @see node_menu()
- *
- * @todo
- *   Add langcode support to node_access schema / queries.
- *   http://drupal.org/node/1658846
  */
 function node_access($op, $node, $account = NULL, $langcode = NULL) {
   if (!$node instanceof EntityInterface) {
@@ -2522,6 +2518,18 @@ function node_access($op, $node, $account = NULL, $langcode = NULL) {
   // If no language code was provided, default to the node's langcode.
   if (empty($langcode)) {
     $langcode = $node->langcode;
+    // If the Language module is enabled, try to use the language from
+    // content negotiation.
+    if (module_exists('language')) {
+      // Load languages the node exists in.
+      $node_translations = $node->getTranslationLanguages();
+      // Load the language from content negotiation.
+      $content_negotiation_langcode = language(LANGUAGE_TYPE_CONTENT)->langcode;
+      // If there is a translation available, use it.
+      if (isset($node_translations[$content_negotiation_langcode])) {
+        $langcode = $content_negotiation_langcode;
+      }
+    }
   }
 
   // Make sure that if an account is passed, that it is a fully loaded user
@@ -2755,6 +2763,9 @@ function node_query_node_access_alter(AlterableInterface $query) {
   if (!$op = $query->getMetaData('op')) {
     $op = 'view';
   }
+  if (!$langcode = $query->getMetaData('langcode')) {
+    $langcode = FALSE;
+  }
 
   // If $account can bypass node access, or there are no node access modules,
   // or the operation is 'view' and the $account has a global view grant
@@ -2792,7 +2803,6 @@ function node_query_node_access_alter(AlterableInterface $query) {
   // Find all instances of the base table being joined -- could appear
   // more than once in the query, and could be aliased. Join each one to
   // the node_access table.
-
   $grants = node_access_grants($op, $account);
   foreach ($tables as $nalias => $tableinfo) {
     $table = $tableinfo['table'];
@@ -2802,8 +2812,8 @@ function node_query_node_access_alter(AlterableInterface $query) {
        ->fields('na', array('nid'));
 
       $grant_conditions = db_or();
-      // If any grant exists for the specified user, then user has access
-      // to the node for the specified operation.
+      // If any grant exists for the specified user, then user has access to the
+      // node for the specified operation.
       foreach ($grants as $realm => $gids) {
         foreach ($gids as $gid) {
           $grant_conditions->condition(db_and()
@@ -2818,6 +2828,20 @@ function node_query_node_access_alter(AlterableInterface $query) {
         $subquery->condition($grant_conditions);
       }
       $subquery->condition('na.grant_' . $op, 1, '>=');
+
+      // Add langcode-based filtering if this is a multilingual site.
+      if (language_multilingual()) {
+        // If no specific langcode to check for is given, use the grant entry
+        // which is set as a fallback.
+        // If a specific langcode is given, use the grant entry for it.
+        if ($langcode === FALSE) {
+          $subquery->condition('na.fallback', 1, '=');
+        }
+        else {
+          $subquery->condition('na.langcode', $langcode, '=');
+        }
+      }
+
       $field = 'nid';
       // Now handle entities.
       $subquery->where("$nalias.$field = na.nid");
@@ -2868,11 +2892,8 @@ function node_access_acquire_grants(EntityInterface $node, $delete = TRUE) {
  * @param \Drupal\Core\Entity\EntityInterface $node
  *   The node whose grants are being written.
  * @param $grants
- *   A list of grants to write. Each grant is an array that must contain the
- *   following keys: realm, gid, grant_view, grant_update, grant_delete.
- *   The realm is specified by a particular module; the gid is as well, and
- *   is a module-defined id to define grant privileges. each grant_* field
- *   is a boolean value.
+ *   A list of grants to write. See hook_node_access_records() for the
+ *   expected structure of the grants array.
  * @param $realm
  *   (optional) If provided, read/write grants for that realm only. Defaults to
  *   NULL.
@@ -2891,18 +2912,35 @@ function _node_access_write_grants(EntityInterface $node, $grants, $realm = NULL
     }
     $query->execute();
   }
-
   // Only perform work when node_access modules are active.
   if (!empty($grants) && count(module_implements('node_grants'))) {
-    $query = db_insert('node_access')->fields(array('nid', 'realm', 'gid', 'grant_view', 'grant_update', 'grant_delete'));
+    $query = db_insert('node_access')->fields(array('nid', 'langcode', 'fallback', 'realm', 'gid', 'grant_view', 'grant_update', 'grant_delete'));
+    // If we have defined a granted langcode, use it. But if not, add a grant
+    // for every language this node is translated to.
     foreach ($grants as $grant) {
       if ($realm && $realm != $grant['realm']) {
         continue;
       }
-      // Only write grants; denies are implicit.
-      if ($grant['grant_view'] || $grant['grant_update'] || $grant['grant_delete']) {
-        $grant['nid'] = $node->nid;
-        $query->values($grant);
+      if (isset($grant['langcode'])) {
+        $grant_languages = array($grant['langcode'] => language_load($grant['langcode']));
+      }
+      else {
+        $grant_languages = $node->getTranslationLanguages(TRUE);
+      }
+      foreach ($grant_languages as $grant_langcode => $grant_language) {
+        // Only write grants; denies are implicit.
+        if ($grant['grant_view'] || $grant['grant_update'] || $grant['grant_delete']) {
+          $grant['nid'] = $node->nid;
+          $grant['langcode'] = $grant_langcode;
+          // The record with the original langcode is used as the fallback.
+          if ($grant['langcode'] == $node->langcode) {
+            $grant['fallback'] = 1;
+          }
+          else {
+            $grant['fallback'] = 0;
+          }
+          $query->values($grant);
+        }
       }
     }
     $query->execute();
@@ -3484,7 +3522,9 @@ function node_modules_disabled($modules) {
     // At this point, the module is already disabled, but its code is still
     // loaded in memory. Module functions must no longer be called. We only
     // check whether a hook implementation function exists and do not invoke it.
-    if (!node_access_needs_rebuild() && module_hook($module, 'node_grants')) {
+    // Node access also needs to be rebuilt if language module is disabled
+    // to remove any language-specific grants.
+    if (!node_access_needs_rebuild() && (module_hook($module, 'node_grants') || $module == 'language')) {
       node_access_needs_rebuild(TRUE);
     }
   }
diff --git a/core/modules/node/tests/modules/node_access_test_language/node_access_test_language.info.yml b/core/modules/node/tests/modules/node_access_test_language/node_access_test_language.info.yml
new file mode 100644
index 0000000..a1ec076
--- /dev/null
+++ b/core/modules/node/tests/modules/node_access_test_language/node_access_test_language.info.yml
@@ -0,0 +1,8 @@
+name: 'Node module access tests language'
+description: 'Support module for language-aware node access testing.'
+package: Testing
+version: VERSION
+core: 8.x
+dependencies:
+- options
+hidden: true
diff --git a/core/modules/node/tests/modules/node_access_test_language/node_access_test_language.module b/core/modules/node/tests/modules/node_access_test_language/node_access_test_language.module
new file mode 100644
index 0000000..3d67454
--- /dev/null
+++ b/core/modules/node/tests/modules/node_access_test_language/node_access_test_language.module
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * @file
+ * Test module with a language-aware node access implementation.
+ *
+ * The module adds a 'private' field to page nodes that allows each translation
+ * of the node to be marked as private (viewable only by administrators).
+ */
+
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Implements hook_node_grants().
+ *
+ * This module defines a single grant realm. All users belong to this group.
+ */
+function node_access_test_language_node_grants($account, $op) {
+  $grants['node_access_language_test'] = array(7888);
+  return $grants;
+}
+
+/**
+ * Implements hook_node_access_records().
+ */
+function node_access_test_language_node_access_records(EntityInterface $node) {
+  $grants = array();
+
+  // Create grants for each translation of the node.
+  foreach ($node->getTranslationLanguages() as $langcode => $language) {
+    // If the translation is not marked as private, grant access.
+    $grants[] = array(
+      'realm' => 'node_access_language_test',
+      'gid' => 7888,
+      'grant_view' => empty($node->field_private[$langcode][0]['value']) ? 1 : 0,
+      'grant_update' => 0,
+      'grant_delete' => 0,
+      'priority' => 0,
+      'langcode' => $langcode,
+    );
+  }
+  return $grants;
+}
+
+/**
+ * Implements hook_enable().
+ *
+ * Creates the 'private' field, which allows the node to be marked as private
+ * (restricted access) in a given translation.
+ */
+function node_access_test_language_enable() {
+  $field_private = array(
+    'field_name' => 'field_private',
+    'type' => 'list_boolean',
+    'cardinality' => 1,
+    'translatable'  => TRUE,
+    'settings' => array(
+      'allowed_values' => array(0 => 'Not private', 1 => 'Private'),
+    ),
+  );
+  $field_private = field_create_field($field_private);
+
+  $instance = array(
+    'field_name' => $field_private['field_name'],
+    'entity_type' => 'node',
+    'bundle' => 'page',
+    'widget' => array(
+      'type' => 'options_buttons',
+    ),
+  );
+  $instance = field_create_instance($instance);
+}
+
+/**
+ * Implements hook_disable().
+ */
+function node_access_test_language_disable() {
+  field_delete_instance(field_read_instance('node', 'field_private', 'page'));
+}
