diff --git a/core/modules/language/src/LanguageNegotiationMethodBase.php b/core/modules/language/src/LanguageNegotiationMethodBase.php index 8e74840d48..6a4c4e9dd9 100644 --- a/core/modules/language/src/LanguageNegotiationMethodBase.php +++ b/core/modules/language/src/LanguageNegotiationMethodBase.php @@ -32,6 +32,15 @@ */ protected $currentUser; + /** + * Gets the language manager. + * + * @return \Drupal\Core\Language\LanguageManagerInterface + */ + protected function getLanguageManager() { + return $this->languageManager; + } + /** * {@inheritdoc} */ diff --git a/core/modules/language/src/LanguageSwitchLinksTrait.php b/core/modules/language/src/LanguageSwitchLinksTrait.php new file mode 100644 index 0000000000..0101e397dc --- /dev/null +++ b/core/modules/language/src/LanguageSwitchLinksTrait.php @@ -0,0 +1,82 @@ +getLanguageManager()->getNativeLanguages() as $language) { + $langcode = $language->getId(); + $links[$langcode] = [ + // We need to clone the $url object to avoid using the same one for all + // links. When the links are rendered, options are set on the $url + // object, so if we use the same one, they would be set for all links. + 'url' => clone $url, + 'title' => $language->getName(), + 'language' => $language, + 'attributes' => ['class' => ['language-link']], + ]; + + $this->processLanguageSwitchLink($links[$langcode], $request, $type, $url, $language); + } + + return $links; + } + + /** + * Processes a single language switch link. + * + * By default no explicit processing is done. + * + * @param array $link + * The link to process. + * @param \Symfony\Component\HttpFoundation\Request $request + * The current request. + * @param string $type + * The language type. + * @param \Drupal\Core\Url $url + * The URL the switch links will be relative to. + * @param \Drupal\Core\Language\LanguageInterface $language + * The language this link is for. + */ + protected function processLanguageSwitchLink(array &$link, Request $request, $type, Url $url, LanguageInterface $language) { + } + +} diff --git a/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationContentEntity.php b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationContentEntity.php index b78dbb9e40..d4917ae0bd 100644 --- a/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationContentEntity.php +++ b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationContentEntity.php @@ -4,12 +4,14 @@ use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Language\LanguageInterface; use Drupal\Core\PathProcessor\OutboundPathProcessorInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Url; use Drupal\language\LanguageNegotiationMethodBase; use Drupal\language\LanguageSwitcherInterface; +use Drupal\language\LanguageSwitchLinksTrait; use Drupal\Core\Routing\RouteObjectInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; @@ -28,6 +30,8 @@ */ class LanguageNegotiationContentEntity extends LanguageNegotiationMethodBase implements OutboundPathProcessorInterface, LanguageSwitcherInterface, ContainerFactoryPluginInterface { + use LanguageSwitchLinksTrait; + /** * The language negotiation method ID. */ @@ -136,23 +140,12 @@ public function processOutbound($path, &$options = [], Request $request = NULL, /** * {@inheritdoc} */ - public function getLanguageSwitchLinks(Request $request, $type, Url $url) { - $links = []; + protected function processLanguageSwitchLink(array &$link, Request $request, $type, Url $url, LanguageInterface $language) { $query = []; parse_str($request->getQueryString(), $query); - foreach ($this->languageManager->getNativeLanguages() as $language) { - $langcode = $language->getId(); - $query[static::QUERY_PARAMETER] = $langcode; - $links[$langcode] = [ - 'url' => $url, - 'title' => $language->getName(), - 'attributes' => ['class' => ['language-link']], - 'query' => $query, - ]; - } - - return $links; + $query[static::QUERY_PARAMETER] = $language->getId(); + $link['query'] = $query; } /** diff --git a/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationSession.php b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationSession.php index 971e907aae..3b110632fe 100644 --- a/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationSession.php +++ b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationSession.php @@ -8,6 +8,7 @@ use Drupal\Core\Url; use Drupal\language\LanguageNegotiationMethodBase; use Drupal\language\LanguageSwitcherInterface; +use Drupal\language\LanguageSwitchLinksTrait; use Symfony\Component\HttpFoundation\Request; /** @@ -23,6 +24,8 @@ */ class LanguageNegotiationSession extends LanguageNegotiationMethodBase implements OutboundPathProcessorInterface, LanguageSwitcherInterface { + use LanguageSwitchLinksTrait; + /** * Flag used to determine whether query rewriting is active. * @@ -123,34 +126,20 @@ public function processOutbound($path, &$options = [], Request $request = NULL, /** * {@inheritdoc} */ - public function getLanguageSwitchLinks(Request $request, $type, Url $url) { - $links = []; + protected function processLanguageSwitchLink(array &$link, Request $request, $type, Url $url, LanguageInterface $language) { $config = $this->config->get('language.negotiation')->get('session'); $param = $config['parameter']; $language_query = isset($_SESSION[$param]) ? $_SESSION[$param] : $this->languageManager->getCurrentLanguage($type)->getId(); $query = []; parse_str($request->getQueryString(), $query); - foreach ($this->languageManager->getNativeLanguages() as $language) { - $langcode = $language->getId(); - $links[$langcode] = [ - // We need to clone the $url object to avoid using the same one for all - // links. When the links are rendered, options are set on the $url - // object, so if we use the same one, they would be set for all links. - 'url' => clone $url, - 'title' => $language->getName(), - 'attributes' => ['class' => ['language-link']], - 'query' => $query, - ]; - if ($language_query != $langcode) { - $links[$langcode]['query'][$param] = $langcode; - } - else { - $links[$langcode]['attributes']['class'][] = 'session-active'; - } + $link['query'] = $query; + if ($language_query != $language->getId()) { + $link['query'][$param] = $language->getId(); + } + else { + $link['attributes']['class'][] = 'session-active'; } - - return $links; } } diff --git a/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php index 7436a3f3a0..97a5b14419 100644 --- a/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php +++ b/core/modules/language/src/Plugin/LanguageNegotiation/LanguageNegotiationUrl.php @@ -6,9 +6,9 @@ use Drupal\Core\PathProcessor\InboundPathProcessorInterface; use Drupal\Core\PathProcessor\OutboundPathProcessorInterface; use Drupal\Core\Render\BubbleableMetadata; -use Drupal\Core\Url; use Drupal\language\LanguageNegotiationMethodBase; use Drupal\language\LanguageSwitcherInterface; +use Drupal\language\LanguageSwitchLinksTrait; use Symfony\Component\HttpFoundation\Request; /** @@ -27,6 +27,8 @@ */ class LanguageNegotiationUrl extends LanguageNegotiationMethodBase implements InboundPathProcessorInterface, OutboundPathProcessorInterface, LanguageSwitcherInterface { + use LanguageSwitchLinksTrait; + /** * The language negotiation method id. */ @@ -190,27 +192,4 @@ public function processOutbound($path, &$options = [], Request $request = NULL, return $path; } - /** - * {@inheritdoc} - */ - public function getLanguageSwitchLinks(Request $request, $type, Url $url) { - $links = []; - $query = $request->query->all(); - - foreach ($this->languageManager->getNativeLanguages() as $language) { - $links[$language->getId()] = [ - // We need to clone the $url object to avoid using the same one for all - // links. When the links are rendered, options are set on the $url - // object, so if we use the same one, they would be set for all links. - 'url' => clone $url, - 'title' => $language->getName(), - 'language' => $language, - 'attributes' => ['class' => ['language-link']], - 'query' => $query, - ]; - } - - return $links; - } - } diff --git a/core/modules/language/tests/src/Functional/LanguageSwitchingTest.php b/core/modules/language/tests/src/Functional/LanguageSwitchingTest.php index cc0ae0ec09..20bed22638 100644 --- a/core/modules/language/tests/src/Functional/LanguageSwitchingTest.php +++ b/core/modules/language/tests/src/Functional/LanguageSwitchingTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\language\Functional; +use Drupal\Core\Url; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\language\Plugin\LanguageNegotiation\LanguageNegotiationUrl; use Drupal\menu_link_content\Entity\MenuLinkContent; @@ -27,6 +28,7 @@ class LanguageSwitchingTest extends BrowserTestBase { 'block', 'language_test', 'menu_ui', + 'path', ]; /** @@ -41,6 +43,7 @@ protected function setUp(): void { $admin_user = $this->drupalCreateUser([ 'administer blocks', 'administer languages', + 'administer url aliases', 'access administration pages', ]); $this->drupalLogin($admin_user); @@ -174,8 +177,8 @@ protected function doTestLanguageBlockAnonymous($block_label) { } $labels[] = $link->getText(); } - $this->assertSame(['active' => ['en'], 'inactive' => ['fr']], $links, 'Only the current language list item is marked as active on the language switcher block.'); - $this->assertSame(['active' => ['en'], 'inactive' => ['fr']], $anchors, 'Only the current language anchor is marked as active on the language switcher block.'); + $this->assertSame(['active' => [], 'inactive' => ['en', 'fr']], $links, 'Only the current language list item is marked as active on the language switcher block.'); + $this->assertSame(['active' => [], 'inactive' => ['en', 'fr']], $anchors, 'Only the current language anchor is marked as active on the language switcher block.'); $this->assertSame(['English', 'français'], $labels, 'The language links labels are in their own language on the language switcher block.'); } @@ -236,6 +239,74 @@ public function testLanguageBlockWithDomain() { $this->assertSession()->elementAttributeContains('xpath', '//div[@id="block-test-language-block"]/ul/li/a[@hreflang="it"]', 'href', $italian_url); } + /** + * Test language switcher links for session-based negotiation and aliases. + */ + public function testLanguageBlockWithSessionAndAlias() { + // Add the Italian language. + ConfigurableLanguage::createFromLangcode('it')->save(); + + // Rebuild the container so that the new language is picked up by services + // that hold a list of languages. + $this->rebuildContainer(); + + $languages = $this->container->get('language_manager')->getLanguages(); + + // Enable session language detection. + $this->drupalGet('/admin/config/regional/language/detection'); + $edit = [ + 'language_interface[enabled][language-session]' => TRUE, + 'language_interface[weight][language-session]' => -10, + 'language_interface[enabled][language-url]' => FALSE, + ]; + $this->submitForm($edit, 'Save settings'); + + // Enable the language switcher block. + $this->drupalPlaceBlock('language_block:' . LanguageInterface::TYPE_INTERFACE, ['id' => 'test_language_block']); + + // Add aliases for both languages. + $url = '/user/' . $this->loggedInUser->id(); + $english_alias = '/my-profile'; + $this->drupalGet('/admin/config/search/path/add'); + $edit = [ + 'langcode[0][value]' => 'en', + 'path[0][value]' => $url, + 'alias[0][value]' => $english_alias, + ]; + $this->submitForm($edit, 'Save'); + + $italian_alias = '/il-mio-profile'; + $this->drupalGet('/admin/config/search/path/add'); + $edit = [ + 'langcode[0][value]' => 'it', + 'path[0][value]' => $url, + 'alias[0][value]' => $italian_alias, + ]; + $this->submitForm($edit, 'Save'); + + $this->drupalGet($url); + + // @todo Generate the aliased paths using + // Uri::fromRoute('entity.user.canonical', ...) directly. See + // https://www.drupal.org/node/2666628. + // If Drupal is installed in a subdirectory, we must prefix the aliases with + // it. + $path_prefix = rtrim(Url::fromRoute('')->toString(), '/'); + // Verify the English URL is correct. + list($english_link) = $this->xpath('//div[@id=:id]/ul/li/a[@hreflang=:hreflang]', [ + ':id' => 'block-test-language-block', + ':hreflang' => 'en', + ]); + $this->assertEquals((string) $english_link->getAttribute('href'), "$path_prefix$english_alias"); + + // Verify the Italian URL is correct. + list($italian_link) = $this->xpath('//div[@id=:id]/ul/li/a[@hreflang=:hreflang]', [ + ':id' => 'block-test-language-block', + ':hreflang' => 'it', + ]); + $this->assertEquals((string) $italian_link->getAttribute('href'), "$path_prefix$italian_alias?language=it"); + } + /** * Tests active class on links when switching languages. */