diff --git a/beta2beta/beta2beta.install b/beta2beta/beta2beta.install
index a730ed4..472eb9e 100644
--- a/beta2beta/beta2beta.install
+++ b/beta2beta/beta2beta.install
@@ -69,6 +69,13 @@ function beta2beta_update_8802() {
 }
 
 /**
+ * Add the langcode field to a variety of entity base tables.
+ */
+function beta2beta_update_8803() {
+  beta2beta_update_helper('2431329', 8);
+}
+
+/**
  * @} End of "defgroup updates-beta-7-to-8"
  */
 
diff --git a/beta2beta/src/Tests/Update/Update2431329Test.php b/beta2beta/src/Tests/Update/Update2431329Test.php
new file mode 100644
index 0000000..733850d
--- /dev/null
+++ b/beta2beta/src/Tests/Update/Update2431329Test.php
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\beta2beta\Tests\Update\Update2431329Test.
+ */
+
+namespace Drupal\beta2beta\Tests\Update;
+
+use Drupal\beta2beta\Tests\Update\TestTraits\FrontPage;
+use Drupal\beta2beta\Tests\Update\TestTraits\NewNode;
+
+/**
+ * Tests the update hook for issue #2431329.
+ *
+ * @group beta2beta
+ */
+class Update2431329Test extends Beta2BetaUpdateTestBase {
+
+  use FrontPage;
+  use NewNode;
+
+  /**
+   * Turn off strict config schema checking.
+   *
+   * This has to be turned off since there are multiple update hooks that update
+   * views. Since only the final view save will be compliant with the current
+   * schema, an exception would be thrown on the first view to be saved if this
+   * were left on.
+   */
+  protected $strictConfigSchema = FALSE;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $startingBeta = 7;
+
+  /**
+   * {@inheritdoc}
+   *
+   * Loads the filled database instead of bare.
+   */
+  protected function initBetaDb() {
+    $file = __DIR__ . '/../../../tests/fixtures/drupal-8.filled.standard.beta' . static::$startingBeta . '.php.gz';
+    if (!file_exists($file)) {
+      throw new \RuntimeException(SafeMarkup::format('Database dump file @file not found', ['@file' => $file]));
+    }
+    // This database should be the very first to be loaded.
+    array_unshift($this->databaseDumpFiles, $file);
+    $this->databaseDumpFiles = array_unique($this->databaseDumpFiles);
+  }
+
+  /**
+   * Test updates for issue #2431329.
+   *
+   * @see beta2beta_update_8803
+   */
+  public function testUpdate2431329() {
+    $this->runUpdates();
+
+    $this->assertTrue(db_field_exists('node', 'langcode'), 'The langcode field was added to the node table.');
+
+    $this->drupalGet('af/node/1');
+    $this->assertText('Test article 1 (Afrikaans)');
+
+    // Check raw table values.
+    $query = db_select('node', 'n')->fields('n', ['nid', 'langcode'])->orderBy('nid')->execute();
+    $expected = [
+      1 => ['langcode' => 'en'],
+      2 => ['langcode' => 'en'],
+      3 => ['langcode' => 'af'],
+    ];
+    foreach ($expected as $nid => $node) {
+      $row = $query->fetchAssoc();
+      $this->assertEqual($node['langcode'], $row['langcode']);
+    }
+  }
+
+}
diff --git a/head2head.module b/head2head.module
index 6f41ef3..1e218d3 100644
--- a/head2head.module
+++ b/head2head.module
@@ -5,6 +5,7 @@
  * Provides HEAD-HEAD upgrade helpers.
  */
 
+use Drupal\Core\Entity\ContentEntityTypeInterface;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
 
@@ -193,6 +194,38 @@ function head2head_2436835() {
 }
 
 /**
+ * Add the langcode field to a variety of entity base tables.
+ */
+function head2head_2431329() {
+  $entity_manager = \Drupal::entityManager();
+  foreach ($entity_manager->getDefinitions() as $entity_type_id => $entity_type) {
+    if ($entity_type instanceof ContentEntityTypeInterface && $entity_type->getDataTable() && $entity_type->hasKey('langcode')) {
+      if ($base_table = $entity_type->getBaseTable()) {
+        if (!db_field_exists($base_table, 'langcode')) {
+          $schema = [
+            // This will be updated to varchar_ascii by subsequent update hooks.
+            'type' => 'varchar',
+            'length' => 12,
+          ];
+          db_add_field($base_table, 'langcode', $schema);
+
+          // Set values based on field data. `db_update()` doesn't implement any
+          // join interfaces, so `db_query()` must be used.
+          $data_table = $entity_type->getDataTable();
+          $field = $entity_type->getKey('revision');
+          $data_langcode_field = $entity_type->getKey('langcode');
+          db_query("UPDATE {" . $base_table . "} bt INNER JOIN {" . $data_table . "} dt ON (bt.$field = dt.$field AND default_langcode = 1) SET bt.langcode = dt.$data_langcode_field");
+
+          // Set NOT NULL.
+          $schema['not null'] = TRUE;
+          db_change_field($base_table, 'langcode', 'langcode', $schema);
+        }
+      }
+    }
+  }
+}
+
+/**
  * Updates views to use the entity's field data table, if available.
  */
 function head2head_2429447() {
