diff --git a/core/modules/book/book.services.yml b/core/modules/book/book.services.yml
index 8743de3b8a99b9e06fa9c50f34d5a243b536af44..ab23275144ee42fca4e5a12e8c4c542e07258bd4 100644
--- a/core/modules/book/book.services.yml
+++ b/core/modules/book/book.services.yml
@@ -1,18 +1,18 @@
 services:
   book.breadcrumb:
     class: Drupal\book\BookBreadcrumbBuilder
-    arguments: ['@entity_type.manager', '@current_user']
+    arguments: ['@entity_type.manager', '@current_user', '@entity.repository', '@language_manager']
     tags:
       - { name: breadcrumb_builder, priority: 701 }
   book.manager:
     class: Drupal\book\BookManager
-    arguments: ['@entity_type.manager', '@string_translation', '@config.factory', '@book.outline_storage', '@renderer']
+    arguments: ['@entity_type.manager', '@string_translation', '@config.factory', '@book.outline_storage', '@renderer', '@language_manager', '@entity.repository']
   book.outline:
     class: Drupal\book\BookOutline
     arguments: ['@book.manager']
   book.export:
     class: Drupal\book\BookExport
-    arguments: ['@entity_type.manager', '@book.manager']
+    arguments: ['@entity_type.manager', '@book.manager', '@entity.repository']
   book.outline_storage:
     class: Drupal\book\BookOutlineStorage
     arguments: ['@database']
diff --git a/core/modules/book/src/BookBreadcrumbBuilder.php b/core/modules/book/src/BookBreadcrumbBuilder.php
index db446a0651ae1ff71f4a6fdb170997b973a57828..11fe40953856256e063449b8c594d85ab82826d9 100644
--- a/core/modules/book/src/BookBreadcrumbBuilder.php
+++ b/core/modules/book/src/BookBreadcrumbBuilder.php
@@ -4,7 +4,10 @@
 
 use Drupal\Core\Breadcrumb\Breadcrumb;
 use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
+use Drupal\Core\Entity\EntityRepositoryInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
 use Drupal\Core\Link;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Session\AccountInterface;
@@ -31,6 +34,20 @@ class BookBreadcrumbBuilder implements BreadcrumbBuilderInterface {
    */
   protected $account;
 
+  /**
+   * The entity repository service.
+   *
+   * @var \Drupal\Core\Entity\EntityRepositoryInterface
+   */
+  protected $entityRepository;
+
+  /**
+   * The language manager service.
+   *
+   * @var \Drupal\Core\Language\LanguageManagerInterface|null
+   */
+  protected $languageManager;
+
   /**
    * Constructs the BookBreadcrumbBuilder.
    *
@@ -38,10 +55,24 @@ class BookBreadcrumbBuilder implements BreadcrumbBuilderInterface {
    *   The entity type manager service.
    * @param \Drupal\Core\Session\AccountInterface $account
    *   The current user account.
+   * @param \Drupal\Core\Entity\EntityRepositoryInterface|null $entity_repository
+   *   The entity repository service.
+   * @param \Drupal\Core\Language\LanguageManagerInterface|null $language_manager
+   *   The language manager service.
    */
-  public function __construct(EntityTypeManagerInterface $entity_type_manager, AccountInterface $account) {
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, AccountInterface $account, EntityRepositoryInterface $entity_repository = NULL, LanguageManagerInterface $language_manager = NULL) {
     $this->nodeStorage = $entity_type_manager->getStorage('node');
     $this->account = $account;
+    if (!$entity_repository) {
+      @trigger_error('The entity.repository service must be passed to ' . __NAMESPACE__ . '\BookBreadcrumbBuilder::__construct(). It was added in drupal:9.2.0 and will be required before drupal:10.0.0.', E_USER_DEPRECATED);
+      $entity_repository = \Drupal::service('entity.repository');
+    }
+    if (!$language_manager) {
+      @trigger_error('The language.manager service must be passed to ' . __NAMESPACE__ . '\BookBreadcrumbBuilder::__construct(). It was added in drupal:9.2.0 and will be required before drupal:10.0.0.', E_USER_DEPRECATED);
+      $language_manager = \Drupal::service('language.manager');
+    }
+    $this->entityRepository = $entity_repository;
+    $this->languageManager = $language_manager;
   }
 
   /**
@@ -59,7 +90,11 @@ public function build(RouteMatchInterface $route_match) {
     $book_nids = [];
     $breadcrumb = new Breadcrumb();
 
-    $links = [Link::createFromRoute($this->t('Home'), '<front>')];
+    $links = [Link::createFromRoute($this->t('Home'), '<front>', [], [
+      'language' => $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT),
+    ]),
+    ];
+    $breadcrumb->addCacheContexts(['languages:' . LanguageInterface::TYPE_CONTENT]);
     $book = $route_match->getParameter('node')->book;
     $depth = 1;
     // We skip the current node.
@@ -67,7 +102,9 @@ public function build(RouteMatchInterface $route_match) {
       $book_nids[] = $book['p' . $depth];
       $depth++;
     }
+    /** @var \Drupal\node\NodeInterface[] $parent_books */
     $parent_books = $this->nodeStorage->loadMultiple($book_nids);
+    $parent_books = array_map([$this->entityRepository, 'getTranslationFromContext'], $parent_books);
     if (count($parent_books) > 0) {
       $depth = 1;
       while (!empty($book['p' . ($depth + 1)])) {
@@ -76,7 +113,7 @@ public function build(RouteMatchInterface $route_match) {
           $breadcrumb->addCacheableDependency($access);
           if ($access->isAllowed()) {
             $breadcrumb->addCacheableDependency($parent_book);
-            $links[] = Link::createFromRoute($parent_book->label(), 'entity.node.canonical', ['node' => $parent_book->id()]);
+            $links[] = $parent_book->toLink();
           }
         }
         $depth++;
diff --git a/core/modules/book/src/BookExport.php b/core/modules/book/src/BookExport.php
index d130610b64db9ca22ab052264ffca43562245a8b..db5cf3da0d9c0ea98c2e6c4fa21ea82c5f29011a 100644
--- a/core/modules/book/src/BookExport.php
+++ b/core/modules/book/src/BookExport.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\book;
 
+use Drupal\Core\Entity\EntityRepositoryInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\node\NodeInterface;
 
@@ -33,6 +34,13 @@ class BookExport {
    */
   protected $bookManager;
 
+  /**
+   * The entity repository service.
+   *
+   * @var \Drupal\Core\Entity\EntityRepositoryInterface
+   */
+  protected $entityRepository;
+
   /**
    * Constructs a BookExport object.
    *
@@ -40,11 +48,18 @@ class BookExport {
    *   The entity type manager.
    * @param \Drupal\book\BookManagerInterface $book_manager
    *   The book manager.
+   * @param \Drupal\Core\Entity\EntityRepositoryInterface|null $entity_repository
+   *   The entity repository service.
    */
-  public function __construct(EntityTypeManagerInterface $entity_type_manager, BookManagerInterface $book_manager) {
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, BookManagerInterface $book_manager, EntityRepositoryInterface $entity_repository = NULL) {
     $this->nodeStorage = $entity_type_manager->getStorage('node');
     $this->viewBuilder = $entity_type_manager->getViewBuilder('node');
     $this->bookManager = $book_manager;
+    if (!$entity_repository) {
+      @trigger_error('The entity.repository service must be passed to ' . __NAMESPACE__ . '\BookExport::__construct(). It was added in drupal:9.2.0 and will be required before drupal:10.0.0.', E_USER_DEPRECATED);
+      $entity_repository = \Drupal::service('entity.repository');
+    }
+    $this->entityRepository = $entity_repository;
   }
 
   /**
@@ -74,6 +89,7 @@ public function bookExportHtml(NodeInterface $node) {
 
     $tree = $this->bookManager->bookSubtreeData($node->book);
     $contents = $this->exportTraverse($tree, [$this, 'bookNodeExport']);
+    $node = $this->entityRepository->getTranslationFromContext($node);
     return [
       '#theme' => 'book_export_html',
       '#title' => $node->label(),
@@ -96,8 +112,8 @@ public function bookExportHtml(NodeInterface $node) {
    * @param callable $callable
    *   A callback to be called upon visiting a node in the tree.
    *
-   * @return string
-   *   The output generated in visiting each node.
+   * @return array
+   *   The render array generated in visiting each node.
    */
   protected function exportTraverse(array $tree, $callable) {
     // If there is no valid callable, use the default callback.
@@ -105,8 +121,9 @@ protected function exportTraverse(array $tree, $callable) {
 
     $build = [];
     foreach ($tree as $data) {
-      // Note- access checking is already performed when building the tree.
+      // Access checking is already performed when building the tree.
       if ($node = $this->nodeStorage->load($data['link']['nid'])) {
+        $node = $this->entityRepository->getTranslationFromContext($node);
         $children = $data['below'] ? $this->exportTraverse($data['below'], $callable) : '';
         $build[] = call_user_func($callable, $node, $children);
       }
diff --git a/core/modules/book/src/BookManager.php b/core/modules/book/src/BookManager.php
index 504f042ea553299e76ebf13d12c96f7b09c16362..b0cb5976300dc436140b4d18203161a3bcaf8000 100644
--- a/core/modules/book/src/BookManager.php
+++ b/core/modules/book/src/BookManager.php
@@ -4,14 +4,18 @@
 
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Cache\Cache;
+use Drupal\Core\Entity\EntityRepositoryInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
 use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\StringTranslation\TranslationInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Template\Attribute;
+use Drupal\Core\Url;
 use Drupal\node\NodeInterface;
 
 /**
@@ -67,6 +71,20 @@ class BookManager implements BookManagerInterface {
    */
   protected $renderer;
 
+  /**
+   * The entity repository service.
+   *
+   * @var \Drupal\Core\Entity\EntityRepositoryInterface
+   */
+  protected $entityRepository;
+
+  /**
+   * The language manager.
+   *
+   * @var \Drupal\Core\Language\LanguageManagerInterface|mixed|null
+   */
+  protected $languageManager;
+
   /**
    * Constructs a BookManager object.
    *
@@ -80,13 +98,27 @@ class BookManager implements BookManagerInterface {
    *   The book outline storage.
    * @param \Drupal\Core\Render\RendererInterface $renderer
    *   The renderer.
+   * @param \Drupal\Core\Language\LanguageManagerInterface|null $language_manager
+   *   The language manager.
+   * @param \Drupal\Core\Entity\EntityRepositoryInterface|null $entity_repository
+   *   The entity repository service.
    */
-  public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationInterface $translation, ConfigFactoryInterface $config_factory, BookOutlineStorageInterface $book_outline_storage, RendererInterface $renderer) {
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, TranslationInterface $translation, ConfigFactoryInterface $config_factory, BookOutlineStorageInterface $book_outline_storage, RendererInterface $renderer, LanguageManagerInterface $language_manager = NULL, EntityRepositoryInterface $entity_repository = NULL) {
     $this->entityTypeManager = $entity_type_manager;
     $this->stringTranslation = $translation;
     $this->configFactory = $config_factory;
     $this->bookOutlineStorage = $book_outline_storage;
     $this->renderer = $renderer;
+    if (!$language_manager) {
+      @trigger_error('The language_manager service must be passed to ' . __NAMESPACE__ . '\BookManager::__construct(). It was added in drupal:9.2.0 and will be required before drupal:10.0.0.', E_USER_DEPRECATED);
+      $language_manager = \Drupal::service('language_manager');
+    }
+    $this->languageManager = $language_manager;
+    if (!$entity_repository) {
+      @trigger_error('The entity.repository service must be passed to ' . __NAMESPACE__ . '\BookManager::__construct(). It was added in drupal:9.2.0 and will be required before drupal:10.0.0.', E_USER_DEPRECATED);
+      $entity_repository = \Drupal::service('entity.repository');
+    }
+    $this->entityRepository = $entity_repository;
   }
 
   /**
@@ -108,10 +140,11 @@ protected function loadBooks() {
 
     if ($nids) {
       $book_links = $this->bookOutlineStorage->loadMultiple($nids);
+      // Load nodes with proper translation.
       $nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($nids);
-      // @todo: Sort by weight and translated title.
-
-      // @todo: use route name for links, not system path.
+      $nodes = array_map([$this->entityRepository, 'getTranslationFromContext'], $nodes);
+      // @todo Sort by weight and translated title.
+      // @todo use route name for links, not system path.
       foreach ($book_links as $link) {
         $nid = $link['nid'];
         if (isset($nodes[$nid]) && $nodes[$nid]->access('view')) {
@@ -424,7 +457,9 @@ protected function recurseTableOfContents(array $tree, $indent, array &$toc, arr
       }
     }
 
+    // Load nodes with proper translation.
     $nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($nids);
+    $nodes = array_map([$this->entityRepository, 'getTranslationFromContext'], $nodes);
 
     foreach ($tree as $data) {
       $nid = $data['link']['nid'];
@@ -476,14 +511,14 @@ public function deleteFromBook($nid) {
    */
   public function bookTreeAllData($bid, $link = NULL, $max_depth = NULL) {
     $tree = &drupal_static(__METHOD__, []);
-    $language_interface = \Drupal::languageManager()->getCurrentLanguage();
 
     // Use $nid as a flag for whether the data being loaded is for the whole
     // tree.
     $nid = isset($link['nid']) ? $link['nid'] : 0;
-    // Generate a cache ID (cid) specific for this $bid, $link, $language, and
+    // Generate a cache ID (cid) specific for this $bid, $link, language, and
     // depth.
-    $cid = 'book-links:' . $bid . ':all:' . $nid . ':' . $language_interface->getId() . ':' . (int) $max_depth;
+    $langcode = $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId();
+    $cid = implode(':', ['book-links', $bid, 'all', $nid, $langcode, (int) $max_depth]);
 
     if (!isset($tree[$cid])) {
       // If the tree data was not in the static cache, build $tree_parameters.
@@ -587,7 +622,9 @@ protected function buildItems(array $tree) {
       // Allow book-specific theme overrides.
       $element['attributes'] = new Attribute();
       $element['title'] = $data['link']['title'];
-      $element['url'] = 'entity:node/' . $data['link']['nid'];
+      $element['url'] = Url::fromUri('entity:node/' . $data['link']['nid'], [
+        'language' => $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT),
+      ]);
       $element['localized_options'] = !empty($data['link']['localized_options']) ? $data['link']['localized_options'] : [];
       $element['localized_options']['set_active_class'] = TRUE;
       $element['below'] = $data['below'] ? $this->buildItems($data['below']) : [];
@@ -666,14 +703,14 @@ protected function bookTreeBuild($bid, array $parameters = []) {
   protected function doBookTreeBuild($bid, array $parameters = []) {
     // Static cache of already built menu trees.
     $trees = &drupal_static(__METHOD__, []);
-    $language_interface = \Drupal::languageManager()->getCurrentLanguage();
 
     // Build the cache id; sort parents to prevent duplicate storage and remove
     // default parameter values.
     if (isset($parameters['expanded'])) {
       sort($parameters['expanded']);
     }
-    $tree_cid = 'book-links:' . $bid . ':tree-data:' . $language_interface->getId() . ':' . hash('sha256', serialize($parameters));
+    $langcode = $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId();
+    $tree_cid = implode(':', ['book-links', $bid, 'tree-data', $langcode, hash('sha256', serialize($parameters))]);
 
     // If we do not have this tree in the static cache, check {cache_data}.
     if (!isset($trees[$tree_cid])) {
@@ -865,7 +902,7 @@ protected function moveChildren(array $link, array $original) {
     if ($shift > 0) {
       // The order of expressions must be reversed so the new values don't
       // overwrite the old ones before they can be used because "Single-table
-      // UPDATE assignments are generally evaluated from left to right"
+      // UPDATE assignments are generally evaluated from left to right".
       // @see http://dev.mysql.com/doc/refman/5.0/en/update.html
       $expressions = array_reverse($expressions);
     }
@@ -1002,13 +1039,14 @@ protected function doBookTreeCheckAccess(&$tree) {
   public function bookLinkTranslate(&$link) {
     // Check access via the api, since the query node_access tag doesn't check
     // for unpublished nodes.
-    // @todo - load the nodes en-mass rather than individually.
+    // @todo load the nodes en-mass rather than individually.
     // @see https://www.drupal.org/project/drupal/issues/2470896
     $node = $this->entityTypeManager->getStorage('node')->load($link['nid']);
     $link['access'] = $node && $node->access('view');
     // For performance, don't localize a link the user can't access.
     if ($link['access']) {
-      // The node label will be the value for the current user's language.
+      // The node label will be the value for the current language.
+      $node = $this->entityRepository->getTranslationFromContext($node);
       $link['title'] = $node->label();
       $link['options'] = [];
     }
@@ -1128,7 +1166,6 @@ public function bookSubtreeData($link) {
         // Compute the real cid for book subtree data.
         $tree_cid = 'book-links:subtree-data:' . hash('sha256', serialize($data));
         // Cache the data, if it is not already in the cache.
-
         if (!\Drupal::cache('data')->get($tree_cid)) {
           \Drupal::cache('data')->set($tree_cid, $data, Cache::PERMANENT, ['bid:' . $link['bid']]);
         }
diff --git a/core/modules/book/src/Form/BookAdminEditForm.php b/core/modules/book/src/Form/BookAdminEditForm.php
index d5d29c1cc82f5f10aeab426217cad59f2670562f..db535fdad01cd2a4fa3db76f018b17180c85fd5f 100644
--- a/core/modules/book/src/Form/BookAdminEditForm.php
+++ b/core/modules/book/src/Form/BookAdminEditForm.php
@@ -5,6 +5,7 @@
 use Drupal\book\BookManager;
 use Drupal\book\BookManagerInterface;
 use Drupal\Component\Utility\Crypt;
+use Drupal\Core\Entity\EntityRepositoryInterface;
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
@@ -34,6 +35,13 @@ class BookAdminEditForm extends FormBase {
    */
   protected $bookManager;
 
+  /**
+   * The entity repository service.
+   *
+   * @var \Drupal\Core\Entity\EntityRepositoryInterface
+   */
+  protected $entityRepository;
+
   /**
    * Constructs a new BookAdminEditForm.
    *
@@ -41,10 +49,17 @@ class BookAdminEditForm extends FormBase {
    *   The custom block storage.
    * @param \Drupal\book\BookManagerInterface $book_manager
    *   The book manager.
+   * @param \Drupal\Core\Entity\EntityRepositoryInterface|null $entity_repository
+   *   The entity repository service.
    */
-  public function __construct(EntityStorageInterface $node_storage, BookManagerInterface $book_manager) {
+  public function __construct(EntityStorageInterface $node_storage, BookManagerInterface $book_manager, EntityRepositoryInterface $entity_repository = NULL) {
     $this->nodeStorage = $node_storage;
     $this->bookManager = $book_manager;
+    if (!$entity_repository) {
+      @trigger_error('The entity.repository service must be passed to ' . __NAMESPACE__ . '\BookAdminEditForm::__construct(). It was added in drupal:9.2.0 and will be required before drupal:10.0.0.', E_USER_DEPRECATED);
+      $entity_repository = \Drupal::service('entity.repository');
+    }
+    $this->entityRepository = $entity_repository;
   }
 
   /**
@@ -54,7 +69,8 @@ public static function create(ContainerInterface $container) {
     $entity_type_manager = $container->get('entity_type.manager');
     return new static(
       $entity_type_manager->getStorage('node'),
-      $container->get('book.manager')
+      $container->get('book.manager'),
+      $container->get('entity.repository')
     );
   }
 
@@ -116,6 +132,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
           // Update the title if changed.
           if ($row['title']['#default_value'] != $values['title']) {
             $node = $this->nodeStorage->load($values['nid']);
+            $node = $this->entityRepository->getTranslationFromContext($node);
             $node->revision_log = $this->t('Title changed from %original to %current.', ['%original' => $node->label(), '%current' => $values['title']]);
             $node->title = $values['title'];
             $node->book['link_title'] = $values['title'];
diff --git a/core/modules/book/tests/src/Kernel/BookMultilingualTest.php b/core/modules/book/tests/src/Kernel/BookMultilingualTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..38bcdcffc382d1319167a56ce05ba5d396e62c6f
--- /dev/null
+++ b/core/modules/book/tests/src/Kernel/BookMultilingualTest.php
@@ -0,0 +1,334 @@
+<?php
+
+namespace Drupal\Tests\book\Kernel;
+
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Link;
+use Drupal\Core\Routing\RouteMatch;
+use Drupal\Core\Url;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl;
+use Drupal\node\Entity\Node;
+use Drupal\node\Entity\NodeType;
+use Drupal\Tests\user\Traits\UserCreationTrait;
+use Drupal\user\Plugin\LanguageNegotiation\LanguageNegotiationUser;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Tests multilingual books.
+ *
+ * @group book
+ */
+class BookMultilingualTest extends KernelTestBase {
+
+  use UserCreationTrait;
+
+  /**
+   * The translation langcode.
+   */
+  const LANGCODE = 'de';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'system',
+    'user',
+    'node',
+    'field',
+    'text',
+    'book',
+    'language',
+    'content_translation',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    // Create the translation language.
+    $this->installConfig(['language']);
+    ConfigurableLanguage::create(['id' => self::LANGCODE])->save();
+    // Set up language negotiation.
+    $config = $this->config('language.types');
+    $config->set('configurable', [
+      LanguageInterface::TYPE_INTERFACE,
+      LanguageInterface::TYPE_CONTENT,
+    ]);
+    // The language being tested should only be available as the content
+    // language so subsequent tests catch errors where the interface language
+    // is used instead of the content language. For this, the interface
+    // language is set to the user language and ::setCurrentLanguage() will
+    // set the user language to the language not being tested.
+    $config->set('negotiation', [
+      LanguageInterface::TYPE_INTERFACE => [
+        'enabled' => [LanguageNegotiationUser::METHOD_ID => 0],
+      ],
+      LanguageInterface::TYPE_CONTENT => [
+        'enabled' => [LanguageNegotiationUrl::METHOD_ID => 0],
+      ],
+    ]);
+    $config->save();
+    $config = $this->config('language.negotiation');
+    $config->set('url.source', LanguageNegotiationUrl::CONFIG_DOMAIN);
+    $config->set('url.domains', [
+      'en' => 'en.book.test.domain',
+      self::LANGCODE => self::LANGCODE . '.book.test.domain',
+    ]);
+    $config->save();
+    $this->container->get('kernel')->rebuildContainer();
+
+    $this->installEntitySchema('node');
+    $this->installEntitySchema('user');
+    $this->installSchema('book', ['book']);
+    $this->installSchema('node', ['node_access']);
+    $this->installSchema('system', ['sequences']);
+    $this->installConfig(['node', 'book', 'field']);
+    $node_type = NodeType::create([
+      'type' => $this->randomMachineName(),
+      'name' => $this->randomString(),
+    ]);
+    $node_type->save();
+    $this->container->get('content_translation.manager')->setEnabled('node', $node_type->id(), TRUE);
+    $book_config = $this->config('book.settings');
+    $allowed_types = $book_config->get('allowed_types');
+    $allowed_types[] = $node_type->id();
+    $book_config->set('allowed_types', $allowed_types)->save();
+    // To test every possible combination of root-child / child-child, two
+    // trees are needed. The first level below the root needs to have two
+    // leaves and similarly a second level is needed with two-two leaves each:
+    //
+    //        1
+    //      /   \
+    //     /     \
+    //    2       3
+    //   / \     / \
+    //  /   \   /   \
+    // 4     5 6     7
+    //
+    // These are the actual node IDs, these are enforced as auto increment is
+    // not reliable.
+    //
+    // Similarly, the second tree root is node 8, the first two leaves are
+    // 9 and 10, the third level is 11, 12, 13, 14.
+    for ($root = 1; $root <= 8; $root += 7) {
+      for ($i = 0; $i <= 6; $i++) {
+        /** @var \Drupal\node\NodeInterface $node */
+        $node = Node::create([
+          'title' => $this->randomString(),
+          'type' => $node_type->id(),
+        ]);
+        $node->addTranslation(self::LANGCODE, [
+          'title' => $this->randomString(),
+        ]);
+        switch ($i) {
+          case 0:
+            $node->book['bid'] = 'new';
+            $node->book['pid'] = 0;
+            $node->book['depth'] = 1;
+            break;
+
+          case 1:
+          case 2:
+            $node->book['bid'] = $root;
+            $node->book['pid'] = $root;
+            $node->book['depth'] = 2;
+            break;
+
+          case 3:
+          case 4:
+            $node->book['bid'] = $root;
+            $node->book['pid'] = $root + 1;
+            $node->book['depth'] = 3;
+            break;
+
+          case 5:
+          case 6:
+            $node->book['bid'] = $root;
+            $node->book['pid'] = $root + 2;
+            $node->book['depth'] = 3;
+            break;
+        }
+        // This is necessary to make the table of contents consistent across
+        // test runs.
+        $node->book['weight'] = $i;
+        $node->nid->value = $root + $i;
+        $node->enforceIsNew();
+        $node->save();
+      }
+    }
+    \Drupal::currentUser()->setAccount($this->createUser(['access content']));
+  }
+
+  /**
+   * Tests various book manager methods return correct translations.
+   *
+   * @dataProvider langcodesProvider
+   */
+  public function testMultilingualBookManager(string $langcode) {
+    $this->setCurrentLanguage($langcode);
+    /** @var \Drupal\book\BookManagerInterface $bm */
+    $bm = $this->container->get('book.manager');
+    $books = $bm->getAllBooks();
+    $this->assertNotEmpty($books);
+    foreach ($books as $book) {
+      $bid = $book['bid'];
+      $build = $bm->bookTreeOutput($bm->bookTreeAllData($bid));
+      $items = $build['#items'];
+      $this->assertBookItemIsCorrectlyTranslated($items[$bid], $langcode);
+      $this->assertBookItemIsCorrectlyTranslated($items[$bid]['below'][$bid + 1], $langcode);
+      $this->assertBookItemIsCorrectlyTranslated($items[$bid]['below'][$bid + 1]['below'][$bid + 3], $langcode);
+      $this->assertBookItemIsCorrectlyTranslated($items[$bid]['below'][$bid + 1]['below'][$bid + 4], $langcode);
+      $this->assertBookItemIsCorrectlyTranslated($items[$bid]['below'][$bid + 2], $langcode);
+      $this->assertBookItemIsCorrectlyTranslated($items[$bid]['below'][$bid + 2]['below'][$bid + 5], $langcode);
+      $this->assertBookItemIsCorrectlyTranslated($items[$bid]['below'][$bid + 2]['below'][$bid + 6], $langcode);
+      $toc = $bm->getTableOfContents($bid, 4);
+      // Root entry does not have an indent.
+      $this->assertToCEntryIsCorrectlyTranslated($toc, $langcode, $bid, '');
+      // The direct children of the root have one indent.
+      $this->assertToCEntryIsCorrectlyTranslated($toc, $langcode, $bid + 1, '--');
+      $this->assertToCEntryIsCorrectlyTranslated($toc, $langcode, $bid + 2, '--');
+      // Their children have two indents.
+      $this->assertToCEntryIsCorrectlyTranslated($toc, $langcode, $bid + 3, '----');
+      $this->assertToCEntryIsCorrectlyTranslated($toc, $langcode, $bid + 4, '----');
+      $this->assertToCEntryIsCorrectlyTranslated($toc, $langcode, $bid + 5, '----');
+      $this->assertToCEntryIsCorrectlyTranslated($toc, $langcode, $bid + 6, '----');
+      // $bid might be a string.
+      $this->assertSame([$bid + 0, $bid + 1, $bid + 3, $bid + 4, $bid + 2, $bid + 5, $bid + 6], array_keys($toc));
+    }
+  }
+
+  /**
+   * Tests various book breadcrumb builder methods return correct translations.
+   *
+   * @dataProvider langcodesProvider
+   */
+  public function testMultilingualBookBreadcrumbBuilder(string $langcode) {
+    $this->setCurrentLanguage($langcode);
+    // Test a level 3 node.
+    $nid = 7;
+    /** @var \Drupal\node\NodeInterface $node */
+    $node = Node::load($nid);
+    $route = new Route('/node/{node}');
+    $route_match = new RouteMatch('entity.node.canonical', $route, ['node' => $node], ['node' => $nid]);
+    /** @var \Drupal\book\BookBreadcrumbBuilder $bbb */
+    $bbb = $this->container->get('book.breadcrumb');
+    $links = $bbb->build($route_match)->getLinks();
+    $link = array_shift($links);
+    $rendered_link = Link::fromTextAndUrl($link->getText(), $link->getUrl())->toString();
+    $this->assertStringContainsString("http://$langcode.book.test.domain/", $rendered_link);
+    $link = array_shift($links);
+    $this->assertNodeLinkIsCorrectlyTranslated(1, $link->getText(), $link->getUrl(), $langcode);
+    $link = array_shift($links);
+    $this->assertNodeLinkIsCorrectlyTranslated(3, $link->getText(), $link->getUrl(), $langcode);
+    $this->assertEmpty($links);
+  }
+
+  /**
+   * Tests the book export returns correct translations.
+   *
+   * @dataProvider langcodesProvider
+   */
+  public function testMultilingualBookExport(string $langcode) {
+    $this->setCurrentLanguage($langcode);
+    /** @var \Drupal\book\BookExport $be */
+    $be = $this->container->get('book.export');
+    /** @var \Drupal\book\BookManagerInterface $bm */
+    $bm = $this->container->get('book.manager');
+    $books = $bm->getAllBooks();
+    $this->assertNotEmpty($books);
+    foreach ($books as $book) {
+      $contents = $be->bookExportHtml(Node::load($book['bid']))['#contents'][0];
+      $this->assertSame($contents["#node"]->language()->getId(), $langcode);
+      $this->assertSame($contents["#children"][0]["#node"]->language()->getId(), $langcode);
+      $this->assertSame($contents["#children"][1]["#node"]->language()->getId(), $langcode);
+      $this->assertSame($contents["#children"][0]["#children"][0]["#node"]->language()->getId(), $langcode);
+      $this->assertSame($contents["#children"][0]["#children"][1]["#node"]->language()->getId(), $langcode);
+      $this->assertSame($contents["#children"][1]["#children"][0]["#node"]->language()->getId(), $langcode);
+      $this->assertSame($contents["#children"][1]["#children"][1]["#node"]->language()->getId(), $langcode);
+    }
+  }
+
+  /**
+   * Data provider for ::testMultilingualBooks().
+   */
+  public function langcodesProvider() {
+    return [
+      [self::LANGCODE],
+      ['en'],
+    ];
+  }
+
+  /**
+   * Sets the current language.
+   *
+   * @param string $langcode
+   *   The langcode. The content language will be set to this using the
+   *   appropriate domain while the user language will be set to something
+   *   else so subsequent tests catch errors where the interface language
+   *   is used instead of the content language.
+   */
+  protected function setCurrentLanguage(string $langcode): void {
+    \Drupal::requestStack()->push(Request::create("http://$langcode.book.test.domain/"));
+    $language_manager = $this->container->get('language_manager');
+    $language_manager->reset();
+    $current_user = \Drupal::currentUser();
+    $languages = $language_manager->getLanguages();
+    unset($languages[$langcode]);
+    $current_user->getAccount()->set('preferred_langcode', reset($languages)->getId());
+    $this->assertNotSame($current_user->getPreferredLangcode(), $langcode);
+  }
+
+  /**
+   * Asserts a book item is correctly translated.
+   *
+   * @param array $item
+   *   A book tree item.
+   * @param string $langcode
+   *   The language code for the requested translation.
+   */
+  protected function assertBookItemIsCorrectlyTranslated(array $item, string $langcode): void {
+    $this->assertNodeLinkIsCorrectlyTranslated($item['original_link']['nid'], $item['title'], $item['url'], $langcode);
+  }
+
+  /**
+   * Asserts a node link is correctly translated.
+   *
+   * @param int $nid
+   *   The node id.
+   * @param string $title
+   *   The expected title.
+   * @param \Drupal\Core\Url $url
+   *   The URL being tested.
+   * @param string $langcode
+   *   The language code.
+   */
+  protected function assertNodeLinkIsCorrectlyTranslated(int $nid, string $title, Url $url, string $langcode): void {
+    $node = Node::load($nid);
+    $this->assertSame($node->getTranslation($langcode)->label(), $title);
+    $rendered_link = Link::fromTextAndUrl($title, $url)->toString();
+    $this->assertStringContainsString("http://$langcode.book.test.domain/node/$nid", $rendered_link);
+  }
+
+  /**
+   * Asserts one entry in the table of contents is correct.
+   *
+   * @param array $toc
+   *   The entire table of contents array.
+   * @param string $langcode
+   *   The language code for the requested translation.
+   * @param int $nid
+   *   The node ID.
+   * @param string $indent
+   *   The indentation before the actual table of contents label.
+   */
+  protected function assertToCEntryIsCorrectlyTranslated(array $toc, string $langcode, int $nid, string $indent) {
+    $node = Node::load($nid);
+    $node_label = $node->getTranslation($langcode)->label();
+    $this->assertSame($indent . ' ' . $node_label, $toc[$nid]);
+  }
+
+}
diff --git a/core/modules/book/tests/src/Unit/BookManagerTest.php b/core/modules/book/tests/src/Unit/BookManagerTest.php
index d37b0fceac60b8d413a976e9b7830035ea343784..d452c6ec20125c83fd042972ba23f3d9b71652e9 100644
--- a/core/modules/book/tests/src/Unit/BookManagerTest.php
+++ b/core/modules/book/tests/src/Unit/BookManagerTest.php
@@ -19,6 +19,20 @@ class BookManagerTest extends UnitTestCase {
    */
   protected $entityTypeManager;
 
+  /**
+   * The mocked language manager.
+   *
+   * @var \Drupal\Core\Language\LanguageManager|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $languageManager;
+
+  /**
+   * The mocked entity repository.
+   *
+   * @var \Drupal\Core\Entity\EntityRepositoryInterface|\PHPUnit\Framework\MockObject\MockObject
+   */
+  protected $entityRepository;
+
   /**
    * The mocked config factory.
    *
@@ -63,7 +77,9 @@ protected function setUp(): void {
     $this->configFactory = $this->getConfigFactoryStub([]);
     $this->bookOutlineStorage = $this->createMock('Drupal\book\BookOutlineStorageInterface');
     $this->renderer = $this->createMock('\Drupal\Core\Render\RendererInterface');
-    $this->bookManager = new BookManager($this->entityTypeManager, $this->translation, $this->configFactory, $this->bookOutlineStorage, $this->renderer);
+    $this->languageManager = $this->createMock('Drupal\Core\Language\LanguageManagerInterface');
+    $this->entityRepository = $this->createMock('Drupal\Core\Entity\EntityRepositoryInterface');
+    $this->bookManager = new BookManager($this->entityTypeManager, $this->translation, $this->configFactory, $this->bookOutlineStorage, $this->renderer, $this->languageManager, $this->entityRepository);
   }
 
   /**