diff --git a/language_switcher_menu.module b/language_switcher_menu.module
index 5f9c35d47e1ab7d79ce8340d4233e9918a69d301..f5a95ebb319301c82311fd0d757c64357a01c67b 100644
--- a/language_switcher_menu.module
+++ b/language_switcher_menu.module
@@ -9,10 +9,13 @@ use Drupal\Core\Routing\RouteMatchInterface;
 
 /**
  * Implements hook_help().
+ *
+ * @phpstan-param string $route_name
+ * @phpstan-return array<mixed>
  */
 function language_switcher_menu_help($route_name, RouteMatchInterface $route_match) {
   if ($route_name !== 'help.page.language_switcher_menu') {
-    return;
+    return [];
   }
 
   $build = [];
diff --git a/language_switcher_menu.services.yml b/language_switcher_menu.services.yml
index 573f7f9f4ac93e01974b28f3f7bc976f14549144..1fd2257a5f17e82bfb85433ea735adc1114138ef 100644
--- a/language_switcher_menu.services.yml
+++ b/language_switcher_menu.services.yml
@@ -5,3 +5,4 @@ services:
       - '@access_manager'
       - '@current_user'
       - '@entity_type.manager'
+      - '@path.validator'
diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php
index ac7a5ae180ffee2723dc2d6e27b380609ac78bd1..2b7a8eaf48799a7d98b9b52d0411b75706b7e8f9 100644
--- a/src/Form/SettingsForm.php
+++ b/src/Form/SettingsForm.php
@@ -68,6 +68,8 @@ class SettingsForm extends ConfigFormBase {
 
   /**
    * {@inheritdoc}
+   *
+   * @phpstan-return self
    */
   public static function create(ContainerInterface $container) {
     return new static(
@@ -81,6 +83,8 @@ class SettingsForm extends ConfigFormBase {
 
   /**
    * {@inheritdoc}
+   *
+   * @phpstan-return array<string>
    */
   protected function getEditableConfigNames() {
     return [
@@ -90,6 +94,8 @@ class SettingsForm extends ConfigFormBase {
 
   /**
    * {@inheritdoc}
+   *
+   * @phpstan-return string
    */
   public function getFormId() {
     return 'language_switcher_menu_settings_form';
@@ -97,6 +103,9 @@ class SettingsForm extends ConfigFormBase {
 
   /**
    * {@inheritdoc}
+   *
+   * @phpstan-param array<mixed> $form
+   * @phpstan-return array<mixed>
    */
   public function buildForm(array $form, FormStateInterface $form_state) {
     $config = $this->config('language_switcher_menu.settings');
@@ -109,7 +118,7 @@ class SettingsForm extends ConfigFormBase {
     }
     $default = $config->get('type') ? $config->get('type') : NULL;
     if (empty($default) && count($configurable_types) === 1) {
-      $default = isset($type) ? $type : NULL;
+      $default = $type ?? NULL;
     }
     $form['type'] = [
       '#type' => 'select',
@@ -166,6 +175,9 @@ class SettingsForm extends ConfigFormBase {
 
   /**
    * {@inheritdoc}
+   *
+   * @phpstan-param array<mixed> $form
+   * @phpstan-return void
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
     parent::submitForm($form, $form_state);
diff --git a/src/LanguageLinkAccessMenuTreeManipulator.php b/src/LanguageLinkAccessMenuTreeManipulator.php
index 6917a9c00deb018a5dfb87a18ed716d96dbd61e5..58e2294d454b358a695b6b07665bb6e0432723c4 100644
--- a/src/LanguageLinkAccessMenuTreeManipulator.php
+++ b/src/LanguageLinkAccessMenuTreeManipulator.php
@@ -2,38 +2,65 @@
 
 namespace Drupal\language_switcher_menu;
 
+use Drupal\Core\Access\AccessManagerInterface;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Menu\DefaultMenuLinkTreeManipulators;
 use Drupal\Core\Menu\MenuLinkInterface;
-use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Path\PathValidatorInterface;
+use Drupal\Core\Session\AccountInterface;
 use Drupal\language_switcher_menu\Plugin\Menu\LanguageSwitcherLink;
 
 /**
  * Extends access check tree manipulator provided by Drupal Core.
  *
- * @todo Remove once https://www.drupal.org/project/drupal/issues/3008889 has
+ * @todo Revisit once https://www.drupal.org/project/drupal/issues/3008889 has
  *   been fixed.
  */
 class LanguageLinkAccessMenuTreeManipulator extends DefaultMenuLinkTreeManipulators {
 
+  /**
+   * The path validator.
+   *
+   * @var \Drupal\Core\Path\PathValidatorInterface
+   */
+  protected $pathValidator;
+
+  /**
+   * Constructs a LanguageLinkAccessMenuTreeManipulator object.
+   *
+   * @param \Drupal\Core\Access\AccessManagerInterface $access_manager
+   *   The access manager.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The current user.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Path\PathValidatorInterface $path_validator
+   *   The path validator.
+   */
+  public function __construct(AccessManagerInterface $access_manager, AccountInterface $account, EntityTypeManagerInterface $entity_type_manager, PathValidatorInterface $path_validator) {
+    parent::__construct($access_manager, $account, $entity_type_manager);
+    $this->pathValidator = $path_validator;
+  }
+
   /**
    * {@inheritdoc}
    */
   protected function menuLinkCheckAccess(MenuLinkInterface $instance) {
     $access_result = parent::menuLinkCheckAccess($instance);
-    if (!$access_result->isNeutral()) {
+    if (!$instance instanceof LanguageSwitcherLink) {
       return $access_result;
     }
 
-    if ($instance->getUrlObject()->getRouteName() !== '<current>') {
-      return $access_result;
-    }
-    // @note Third party modules may alter language switch links and they would
-    // still be an instance of our class, so we check for it relatively late.
-    if (!$instance instanceof LanguageSwitcherLink) {
-      return $access_result;
+    if (!$instance->hasLink()) {
+      return $access_result->isAllowed() ? AccessResult::neutral() : $access_result;
     }
-    $has_access = $this->account->hasPermission('view language_switcher_menu links');
-    return AccessResult::allowedIf($has_access)->cachePerPermissions();
+
+    $url = $instance->getUrlObject()->setAbsolute(TRUE)->toString();
+    $validated_url = $this->pathValidator->getUrlIfValid($url);
+
+    return AccessResult::allowedIfHasPermission($this->account, 'view language_switcher_menu links')
+      ->andIf(AccessResult::allowedIf($validated_url));
   }
 
 }
diff --git a/src/Plugin/Derivative/LanguageSwitcherLink.php b/src/Plugin/Derivative/LanguageSwitcherLink.php
index afd322dccc00fadf4397c56aed1a3307687fce5e..1a941c7b39b74a9e5439097b425e62a1b66917a4 100644
--- a/src/Plugin/Derivative/LanguageSwitcherLink.php
+++ b/src/Plugin/Derivative/LanguageSwitcherLink.php
@@ -5,9 +5,7 @@ namespace Drupal\language_switcher_menu\Plugin\Derivative;
 use Drupal\Component\Plugin\Derivative\DeriverBase;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Language\LanguageManagerInterface;
-use Drupal\Core\Path\PathMatcherInterface;
 use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
-use Drupal\Core\Url;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
@@ -29,13 +27,6 @@ class LanguageSwitcherLink extends DeriverBase implements ContainerDeriverInterf
    */
   protected $languageManager;
 
-  /**
-   * The path matcher.
-   *
-   * @var \Drupal\Core\Path\PathMatcherInterface
-   */
-  protected $pathMatcher;
-
   /**
    * Constructs a new LanguageSwitcherLink instance.
    *
@@ -43,13 +34,10 @@ class LanguageSwitcherLink extends DeriverBase implements ContainerDeriverInterf
    *   The factory for configuration objects.
    * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
    *   The language manager.
-   * @param \Drupal\Core\Path\PathMatcherInterface $path_matcher
-   *   The path matcher.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, LanguageManagerInterface $language_manager, PathMatcherInterface $path_matcher) {
+  public function __construct(ConfigFactoryInterface $config_factory, LanguageManagerInterface $language_manager) {
     $this->configFactory = $config_factory;
     $this->languageManager = $language_manager;
-    $this->pathMatcher = $path_matcher;
   }
 
   /**
@@ -58,13 +46,15 @@ class LanguageSwitcherLink extends DeriverBase implements ContainerDeriverInterf
   public static function create(ContainerInterface $container, $base_plugin_id) {
     return new static(
       $container->get('config.factory'),
-      $container->get('language_manager'),
-      $container->get('path.matcher'),
+      $container->get('language_manager')
     );
   }
 
   /**
    * {@inheritdoc}
+   *
+   * @phpstan-param mixed $base_plugin_definition
+   * @phpstan-return array<string, mixed>
    */
   public function getDerivativeDefinitions($base_plugin_definition) {
     $links = [];
@@ -82,12 +72,6 @@ class LanguageSwitcherLink extends DeriverBase implements ContainerDeriverInterf
       return $links;
     }
 
-    $route_name = $this->pathMatcher->isFrontPage() ? '<front>' : '<current>';
-    $link_info = $this->languageManager->getLanguageSwitchLinks($config->get('type'), Url::fromRoute($route_name));
-    if (!isset($link_info->links)) {
-      return $links;
-    }
-
     $parent_config = explode(':', $config->get('parent'));
     $menu_name = $parent_config[0];
     $parent = NULL;
@@ -97,23 +81,22 @@ class LanguageSwitcherLink extends DeriverBase implements ContainerDeriverInterf
     }
 
     $weight = (int) $config->get('weight');
-
-    foreach ($link_info->links as $id => $link) {
-      $links[$id] = [
-        'title' => $link['title'],
-        'route_name' => $link['url']->getRouteName(),
-        'route_parameters' => $link['url']->getRouteParameters(),
+    $type = $config->get('type');
+
+    foreach ($this->languageManager->getLanguages() as $langcode => $language) {
+      $links[$langcode] = [
+        'title' => $language->getName(),
+        'metadata' => [
+          'language' => $language,
+          'langcode' => $langcode,
+          'langtype' => $type,
+        ],
+        'route_name' => '<current>',
+        'route_parameters' => [],
         'menu_name' => $menu_name,
         'parent' => $parent,
         'weight' => $weight,
-        'options' => [
-          'language' => $link['language'],
-          'set_active_class' => TRUE,
-        ] + (isset($link['query']) ? [
-          'query' => $link['query'],
-        ] : []) + (isset($link['attributes']) ? [
-          'attributes' => $link['attributes'],
-        ] : []),
+        'options' => [],
       ] + $base_plugin_definition;
       $weight + 1;
     }
diff --git a/src/Plugin/Menu/LanguageSwitcherLink.php b/src/Plugin/Menu/LanguageSwitcherLink.php
index 005521fc56509ffd6b4db824a6519549f14776cf..1a904ce9ca1613b467660a65c62d0033a695b152 100644
--- a/src/Plugin/Menu/LanguageSwitcherLink.php
+++ b/src/Plugin/Menu/LanguageSwitcherLink.php
@@ -2,13 +2,188 @@
 
 namespace Drupal\language_switcher_menu\Plugin\Menu;
 
+use Drupal\Core\Url;
+use Drupal\Core\Language\LanguageManagerInterface;
 use Drupal\Core\Menu\MenuLinkDefault;
+use Drupal\Core\Menu\StaticMenuLinkOverridesInterface;
+use Drupal\Core\Path\PathMatcherInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Represents a menu link to switch to a specific language.
  */
 class LanguageSwitcherLink extends MenuLinkDefault {
 
+  /**
+   * The language manager.
+   *
+   * @var \Drupal\Core\Language\LanguageManagerInterface
+   */
+  protected $languageManager;
+
+  /**
+   * The path matcher.
+   *
+   * @var \Drupal\Core\Path\PathMatcherInterface
+   */
+  protected $pathMatcher;
+
+  /**
+   * Language switch links for active route.
+   *
+   * NULL, if never initialized. FALSE, if unsuccessfully initialized. An array
+   * of language switch links, if successfully initialized.
+   *
+   * @var array|null|false
+   * @phpstan-var array<string,mixed>|null|false
+   */
+  protected $links = NULL;
+
+  /**
+   * Constructs a new LanguageSwitcherLink.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
+   * @param \Drupal\Core\Menu\StaticMenuLinkOverridesInterface $static_override
+   *   The static override storage.
+   * @param \Drupal\Core\Path\PathMatcherInterface $path_matcher
+   *   The path matcher.
+   *
+   * @phpstan-param array<mixed> $configuration
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, LanguageManagerInterface $language_manager, StaticMenuLinkOverridesInterface $static_override, PathMatcherInterface $path_matcher) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition, $static_override);
+    $this->languageManager = $language_manager;
+    $this->pathMatcher = $path_matcher;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @phpstan-param array<mixed> $configuration
+   * @phpstan-param string $plugin_id
+   * @phpstan-param mixed $plugin_definition
+   * @phpstan-return self
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('language_manager'),
+      $container->get('menu_link.static.overrides'),
+      $container->get('path.matcher')
+    );
+  }
+
+  /**
+   * Initializes the "links" property.
+   *
+   * If the "links" property has not been initialized yet, gets links from
+   * language switcher and assigns them to the "links" property.
+   */
+  protected function initLinks(): void {
+    if ($this->links !== NULL) {
+      return;
+    }
+    $route_name = $this->pathMatcher->isFrontPage() ? '<front>' : '<current>';
+    $link_info = $this->languageManager->getLanguageSwitchLinks($this->getLanguageType(), Url::fromRoute($route_name));
+    $this->links = $link_info->links ?? FALSE;
+  }
+
+  /**
+   * Whether a language switch link for this menu link's language code exists.
+   *
+   * @return bool
+   *   Whether a language switch link for this menu link's language code exists.
+   */
+  public function hasLink(): bool {
+    $this->initLinks();
+    return isset($this->links[$this->getLangCode()]);
+  }
+
+  /**
+   * Gets link for language code of this language switcher menu link.
+   *
+   * @return array
+   *   Link for language code of this language switcher menu link.
+   *
+   * @phpstan-return array<mixed>
+   */
+  protected function getLink() {
+    $this->initLinks();
+    return $this->hasLink() ? $this->links[$this->getLangCode()] : [];
+  }
+
+  /**
+   * Gets the language code.
+   *
+   * @return string
+   *   Language code.
+   */
+  protected function getLangCode(): string {
+    return $this->pluginDefinition['metadata']['langcode'];
+  }
+
+  /**
+   * Gets the language type.
+   *
+   * @return string
+   *   Language type.
+   */
+  protected function getLanguageType(): string {
+    return $this->pluginDefinition['metadata']['langtype'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTitle() {
+    $link = $this->getLink();
+    return (string) ($link['title'] ?? parent::getTitle());
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @phpstan-return array<string, mixed>
+   */
+  public function getOptions() {
+    $link = $this->getLink();
+    return [
+      'language' => $link['language'] ?? NULL,
+      'set_active_class' => TRUE,
+    ] + (isset($link['query']) ? [
+      'query' => $link['query'],
+    ] : []) + (isset($link['attributes']) ? [
+      'attributes' => $link['attributes'],
+    ] : []) + parent::getOptions();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRouteName() {
+    $link = $this->getLink();
+    return isset($link['url']) ? $link['url']->getRouteName() : '';
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @phpstan-return array<string, mixed>
+   */
+  public function getRouteParameters() {
+    $link = $this->getLink();
+    return isset($link['url']) ? $link['url']->getRouteParameters() : [];
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/tests/src/Functional/LanguageSwitcherMenuTest.php b/tests/src/Functional/LanguageSwitcherMenuTest.php
index 8919af96eadd9577c31783490e1060f84df5a13c..448e892477ab9b7e9990791b837e798386cd4aec 100644
--- a/tests/src/Functional/LanguageSwitcherMenuTest.php
+++ b/tests/src/Functional/LanguageSwitcherMenuTest.php
@@ -70,7 +70,7 @@ class LanguageSwitcherMenuTest extends BrowserTestBase {
   /**
    * Tests language switch links provided by Language Switcher Menu module.
    */
-  public function testLanguageSwitchLinks() {
+  public function testLanguageSwitchLinks(): void {
     $this->drupalLogin($this->users['admin_user']);
 
     // Add a language.
@@ -155,6 +155,8 @@ class LanguageSwitcherMenuTest extends BrowserTestBase {
     $this->assertSession()->statusCodeEquals(403);
 
     $this->drupalLogin($this->users['view_links']);
+    $this->drupalGet('/user/' . $this->users['view_links']->id());
+    $this->assertSession()->statusCodeEquals(200);
     $this->assertMenuLinks([
       '/user/' . $this->users['view_links']->id() => 'English',
       '/fr/user/' . $this->users['view_links']->id() => 'French',
@@ -168,16 +170,16 @@ class LanguageSwitcherMenuTest extends BrowserTestBase {
     $this->drupalGet('user/' . $admin_id . '/edit');
     $this->assertSession()->statusCodeEquals(403);
     $this->assertMenuLinks([
-      '/system/403' => 'English',
-      '/fr/system/403' => 'French',
+      '/system/403?destination=/user/' . $admin_id . '/edit&_exception_statuscode=403' => 'English',
+      '/fr/system/403?destination=/user/' . $admin_id . '/edit&_exception_statuscode=403' => 'French',
       '/' => 'Home',
     ], 'The language links are visible on pages that deny access.');
 
     // Check that links are shown in a non-standard language.
     $this->clickLink('French');
     $this->assertMenuLinks([
-      '/system/403' => 'English',
-      '/fr/system/403' => 'French',
+      '/system/403?destination=/user/' . $admin_id . '/edit&_exception_statuscode=403' => 'English',
+      '/fr/system/403?destination=/user/' . $admin_id . '/edit&_exception_statuscode=403' => 'French',
       '/fr' => 'Home',
     ], 'The menu links are correct in a non-standard language.');
 
@@ -254,7 +256,7 @@ class LanguageSwitcherMenuTest extends BrowserTestBase {
    * @param string $text
    *   Assert message.
    */
-  protected function assertMenuLinks(array $expected, string $text) {
+  protected function assertMenuLinks(array $expected, string $text): void {
     $language_switchers = $this->xpath('//nav/ul/li');
     $labels = [];
     $urls = [];
@@ -267,7 +269,13 @@ class LanguageSwitcherMenuTest extends BrowserTestBase {
     // through \Drupal\Core\Url to get the correct URL in use.
     $expected_compare = [];
     foreach ($expected as $url => $label) {
-      $url = Url::fromUserInput($url)->toString();
+      $url = Url::fromUserInput($url);
+      $options = $url->getOptions();
+      if (isset($options['query']['destination'])) {
+        $options['query']['destination'] = Url::fromUserInput($options['query']['destination'])->toString();
+      }
+      $url->setOptions($options);
+      $url = $url->toString();
       $expected_compare[$url] = $label;
     }
     $this->assertSame(array_keys($expected_compare), $urls, $text);