diff --git a/core/.phpstan-baseline.php b/core/.phpstan-baseline.php index 51733d9a9edfa9ddf41faf6c972a1b75d44f69aa..c3c07f70a141d9b8488de47a158c3ed57fcb65ed 100644 --- a/core/.phpstan-baseline.php +++ b/core/.phpstan-baseline.php @@ -14515,6 +14515,18 @@ 'count' => 1, 'path' => __DIR__ . '/modules/contextual/src/Plugin/views/field/ContextualLinks.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method Drupal\\\\Tests\\\\contextual\\\\Functional\\\\LanguageAdministrationPagesLanguageTest\\:\\:clickContextualLink\\(\\) has no return type specified\\.$#', + 'identifier' => 'missingType.return', + 'count' => 1, + 'path' => __DIR__ . '/modules/contextual/tests/src/Functional/LanguageAdministrationPagesLanguageTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Drupal\\\\Tests\\\\contextual\\\\Functional\\\\LanguageAdministrationPagesLanguageTest\\:\\:toggleContextualTriggerVisibility\\(\\) has no return type specified\\.$#', + 'identifier' => 'missingType.return', + 'count' => 1, + 'path' => __DIR__ . '/modules/contextual/tests/src/Functional/LanguageAdministrationPagesLanguageTest.php', +]; $ignoreErrors[] = [ 'message' => '#^Method Drupal\\\\Tests\\\\contextual\\\\FunctionalJavascript\\\\ContextualLinksTest\\:\\:clickContextualLink\\(\\) has no return type specified\\.$#', 'identifier' => 'missingType.return', @@ -14527,6 +14539,18 @@ 'count' => 1, 'path' => __DIR__ . '/modules/contextual/tests/src/FunctionalJavascript/ContextualLinksTest.php', ]; +$ignoreErrors[] = [ + 'message' => '#^Method Drupal\\\\Tests\\\\contextual\\\\FunctionalJavascript\\\\ContextualTranslationTest\\:\\:clickContextualLink\\(\\) has no return type specified\\.$#', + 'identifier' => 'missingType.return', + 'count' => 1, + 'path' => __DIR__ . '/modules/contextual/tests/src/FunctionalJavascript/ContextualTranslationTest.php', +]; +$ignoreErrors[] = [ + 'message' => '#^Method Drupal\\\\Tests\\\\contextual\\\\FunctionalJavascript\\\\ContextualTranslationTest\\:\\:toggleContextualTriggerVisibility\\(\\) has no return type specified\\.$#', + 'identifier' => 'missingType.return', + 'count' => 1, + 'path' => __DIR__ . '/modules/contextual/tests/src/FunctionalJavascript/ContextualTranslationTest.php', +]; $ignoreErrors[] = [ 'message' => '#^Method Drupal\\\\datetime\\\\DateTimeComputed\\:\\:setValue\\(\\) has no return type specified\\.$#', 'identifier' => 'missingType.return', diff --git a/core/core.services.yml b/core/core.services.yml index a20bcaf6ae9f5937f505a315d081eecd21d4f3bb..42527f5a67ba9027522613e5620f53410811d554 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -198,6 +198,11 @@ services: arguments: ['@current_user'] tags: - { name: cache.context} + cache_context.user.admin_language: + class: Drupal\Core\Cache\Context\AccountAdminLanguageCacheContext + arguments: [ '@current_user' ] + tags: + - { name: cache.context } cache_context.user.permissions: class: Drupal\Core\Cache\Context\AccountPermissionsCacheContext arguments: ['@current_user', '@user_permissions_hash_generator'] diff --git a/core/lib/Drupal/Core/Cache/Context/AccountAdminLanguageCacheContext.php b/core/lib/Drupal/Core/Cache/Context/AccountAdminLanguageCacheContext.php new file mode 100644 index 0000000000000000000000000000000000000000..647257a0ae60e1ab5280f6c2cef840d534cecbd5 --- /dev/null +++ b/core/lib/Drupal/Core/Cache/Context/AccountAdminLanguageCacheContext.php @@ -0,0 +1,44 @@ +currentUser->getPreferredAdminLangcode(FALSE); + } + + /** + * {@inheritdoc} + */ + public function getCacheableMetadata(): CacheableMetadata { + return new CacheableMetadata(); + } + +} diff --git a/core/lib/Drupal/Core/Language/LanguageManager.php b/core/lib/Drupal/Core/Language/LanguageManager.php index 1f812b7c2c98197e70fbc99863a797c54388ac35..cbe1961c2aa2144e4bfaffc8dcecc21702f0b032 100644 --- a/core/lib/Drupal/Core/Language/LanguageManager.php +++ b/core/lib/Drupal/Core/Language/LanguageManager.php @@ -444,4 +444,12 @@ protected function filterLanguages(array $languages, $flags = LanguageInterface: return $filtered_languages; } + /** + * {@inheritdoc} + */ + public function setCurrentLanguage( + LanguageInterface $language, + ?string $type = LanguageInterface::TYPE_INTERFACE, + ): void {} + } diff --git a/core/lib/Drupal/Core/Language/LanguageManagerInterface.php b/core/lib/Drupal/Core/Language/LanguageManagerInterface.php index 270e8920f5d5fb774af71d64c3c8ac39136f5b31..6339102c170cfbb02a2ccb0a78553dfce5b5184a 100644 --- a/core/lib/Drupal/Core/Language/LanguageManagerInterface.php +++ b/core/lib/Drupal/Core/Language/LanguageManagerInterface.php @@ -212,4 +212,19 @@ public function getConfigOverrideLanguage(); */ public static function getStandardLanguageList(); + /** + * Sets the current language for the given type. + * + * @param \Drupal\Core\Language\LanguageInterface $language + * The current language object for the given type of language. + * @param string $type + * (optional) The language type; e.g., the interface or the content + * language. Defaults to + * \Drupal\Core\Language\LanguageInterface::TYPE_INTERFACE. + */ + public function setCurrentLanguage( + LanguageInterface $language, + ?string $type = LanguageInterface::TYPE_INTERFACE, + ): void; + } diff --git a/core/modules/block/tests/src/Functional/Views/DisplayBlockTest.php b/core/modules/block/tests/src/Functional/Views/DisplayBlockTest.php index 58749d8485aa9049edbdcd1cfb61468ac24266f1..3cf006c4b5bc0194ca82eda13f1bae841c8e1443 100644 --- a/core/modules/block/tests/src/Functional/Views/DisplayBlockTest.php +++ b/core/modules/block/tests/src/Functional/Views/DisplayBlockTest.php @@ -361,7 +361,7 @@ public function testBlockEmptyRendering(): void { 'http_response', 'rendered', ])); - $this->assertCacheContexts(['url.query_args:_wrapper_format']); + $this->assertCacheContexts(['url.query_args:_wrapper_format', 'user.admin_language']); // Hide the header on empty results. $display = &$view->getDisplay('block_1'); @@ -406,7 +406,7 @@ public function testBlockEmptyRendering(): void { 'http_response', 'rendered', ])); - $this->assertCacheContexts(['url.query_args:_wrapper_format']); + $this->assertCacheContexts(['url.query_args:_wrapper_format', 'user.admin_language']); } /** @@ -422,9 +422,9 @@ public function testBlockContextualLinks(): void { $cached_block = $this->drupalPlaceBlock('views_block:test_view_block-block_1'); $this->drupalGet('test-page'); - $id = 'block:block=' . $block->id() . ':langcode=en|entity.view.edit_form:view=test_view_block:location=block&name=test_view_block&display_id=block_1&langcode=en'; + $id = 'block:block=' . $block->id() . ':langcode=en&admin_langcode=en|entity.view.edit_form:view=test_view_block:location=block&name=test_view_block&display_id=block_1&langcode=en&admin_langcode=en'; $id_token = Crypt::hmacBase64($id, Settings::getHashSalt() . $this->container->get('private_key')->get()); - $cached_id = 'block:block=' . $cached_block->id() . ':langcode=en|entity.view.edit_form:view=test_view_block:location=block&name=test_view_block&display_id=block_1&langcode=en'; + $cached_id = 'block:block=' . $cached_block->id() . ':langcode=en&admin_langcode=en|entity.view.edit_form:view=test_view_block:location=block&name=test_view_block&display_id=block_1&langcode=en&admin_langcode=en'; $cached_id_token = Crypt::hmacBase64($cached_id, Settings::getHashSalt() . $this->container->get('private_key')->get()); // @see \Drupal\contextual\Tests\ContextualDynamicContextTest:assertContextualLinkPlaceHolder() // Check existence of the contextual link placeholders. diff --git a/core/modules/block_content/tests/src/Functional/BlockContentContextualLinksTest.php b/core/modules/block_content/tests/src/Functional/BlockContentContextualLinksTest.php index 4bbd99068243e055dfed37cfbfd44ffbe5c7c81d..f645454b3b0ce95962bd497bd250e08b847159d1 100644 --- a/core/modules/block_content/tests/src/Functional/BlockContentContextualLinksTest.php +++ b/core/modules/block_content/tests/src/Functional/BlockContentContextualLinksTest.php @@ -41,7 +41,7 @@ public function testBlockContentContextualLinks(): void { $this->drupalLogin($user); $this->drupalGet(''); - $this->assertSession()->elementAttributeContains('css', 'div[data-contextual-id]', 'data-contextual-id', 'block:block=' . $block->id() . ':langcode=en|block_content:block_content=' . $block_content->id() . ':'); + $this->assertSession()->elementAttributeContains('css', 'div[data-contextual-id]', 'data-contextual-id', 'block:block=' . $block->id() . ':langcode=en&admin_langcode=en|block_content:block_content=' . $block_content->id() . ':'); } } diff --git a/core/modules/contextual/src/ContextualLinksSerializer.php b/core/modules/contextual/src/ContextualLinksSerializer.php index 18d86843da354b2d3595e74e77f1d36a67f16138..b7b3dbd35a66dcb5bd08640a9ddeeb81a95d2885 100644 --- a/core/modules/contextual/src/ContextualLinksSerializer.php +++ b/core/modules/contextual/src/ContextualLinksSerializer.php @@ -42,13 +42,18 @@ public function linksToId(array $contextualLinks): string { $ids = []; $langcode = $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_URL)->getId(); + $admin_langcode = \Drupal::currentUser()->getPreferredAdminLangcode(); foreach ($contextualLinks as $group => $args) { $routeParameters = UrlHelper::buildQuery($args['route_parameters']); $args += ['metadata' => []]; // Add the current URL language to metadata so a different ID will be // computed when URLs vary by language. This allows to store different - // language-aware contextual links on the client side. - $args['metadata'] += ['langcode' => $langcode]; + // language-aware contextual links on the client side. Add the admin + // language as the link text can vary by that. + $args['metadata'] += [ + 'langcode' => $langcode, + 'admin_langcode' => $admin_langcode, + ]; $metadata = UrlHelper::buildQuery($args['metadata']); $ids[] = "$group:$routeParameters:$metadata"; } diff --git a/core/modules/contextual/src/Hook/ContextualThemeHooks.php b/core/modules/contextual/src/Hook/ContextualThemeHooks.php index 607986bd1716ca7961d4dfd9b4a697f71882c64f..933b8e0254ad49ba599d2ef28b4b79ece5029169 100644 --- a/core/modules/contextual/src/Hook/ContextualThemeHooks.php +++ b/core/modules/contextual/src/Hook/ContextualThemeHooks.php @@ -39,6 +39,7 @@ public function preprocess(&$variables, $hook, $info): void { if (isset($element) && is_array($element) && !empty($element['#contextual_links'])) { $variables['#cache']['contexts'][] = 'user.permissions'; + $variables['#cache']['contexts'][] = 'user.admin_language'; if ($this->currentUser->hasPermission('access contextual links')) { // Mark this element as potentially having contextual links attached to // it. diff --git a/core/modules/contextual/tests/src/Functional/ContextualDynamicContextTest.php b/core/modules/contextual/tests/src/Functional/ContextualDynamicContextTest.php index a5dbdc46c0dd80a48e4118630e444fe3f46ddc55..f85a9602790c0e61014926682e6d67af45765ea4 100644 --- a/core/modules/contextual/tests/src/Functional/ContextualDynamicContextTest.php +++ b/core/modules/contextual/tests/src/Functional/ContextualDynamicContextTest.php @@ -103,10 +103,10 @@ public function testDifferentPermissions(): void { // Now, on the front page, all article nodes should have contextual links // placeholders, as should the view that contains them. $ids = [ - 'node:node=' . $node1->id() . ':changed=' . $node1->getChangedTime() . '&langcode=en', - 'node:node=' . $node2->id() . ':changed=' . $node2->getChangedTime() . '&langcode=en', - 'node:node=' . $node3->id() . ':changed=' . $node3->getChangedTime() . '&langcode=en', - 'entity.view.edit_form:view=frontpage:location=page&name=frontpage&display_id=page_1&langcode=en', + 'node:node=' . $node1->id() . ':changed=' . $node1->getChangedTime() . '&langcode=en&admin_langcode=en', + 'node:node=' . $node2->id() . ':changed=' . $node2->getChangedTime() . '&langcode=en&admin_langcode=en', + 'node:node=' . $node3->id() . ':changed=' . $node3->getChangedTime() . '&langcode=en&admin_langcode=en', + 'entity.view.edit_form:view=frontpage:location=page&name=frontpage&display_id=page_1&langcode=en&admin_langcode=en', ]; // Editor user: can access contextual links and can edit articles. @@ -130,7 +130,7 @@ public function testDifferentPermissions(): void { 'title' => $this->randomString(), 'promote' => TRUE, ])->save(); - $id = 'node:node=' . $node3->id() . ':changed=' . $node3->getChangedTime() . '&langcode=it'; + $id = 'node:node=' . $node3->id() . ':changed=' . $node3->getChangedTime() . '&langcode=it&admin_langcode=en'; $this->drupalGet('node', ['language' => ConfigurableLanguage::createFromLangcode('it')]); $this->assertContextualLinkPlaceHolder($id); @@ -184,7 +184,7 @@ public function testTokenProtection(): void { // Now, on the front page, all article nodes should have contextual links // placeholders, as should the view that contains them. - $id = 'node:node=' . $node1->id() . ':changed=' . $node1->getChangedTime() . '&langcode=en'; + $id = 'node:node=' . $node1->id() . ':changed=' . $node1->getChangedTime() . '&langcode=en&admin_langcode=en'; // Editor user: can access contextual links and can edit articles. $this->drupalGet('node'); diff --git a/core/modules/contextual/tests/src/Functional/LanguageAdministrationPagesLanguageTest.php b/core/modules/contextual/tests/src/Functional/LanguageAdministrationPagesLanguageTest.php new file mode 100644 index 0000000000000000000000000000000000000000..cdab3c62c40e280c5310c6a49a3ac6d483ae5f90 --- /dev/null +++ b/core/modules/contextual/tests/src/Functional/LanguageAdministrationPagesLanguageTest.php @@ -0,0 +1,151 @@ +admin = $this->drupalCreateUser([], NULL, TRUE); + $this->drupalLogin($this->admin); + + // Create FR. + ConfigurableLanguage::createFromLangcode('fr')->save(); + + // Set language detection to URL and browser detection. + $this->drupalGet('/admin/config/regional/language/detection'); + $this->submitForm([ + 'language_interface[enabled][language-url]' => TRUE, + 'language_interface[enabled][language-user-admin]' => TRUE, + 'language_content[configurable]' => TRUE, + 'language_content[enabled][language-url]' => TRUE, + 'language_content[enabled][language-interface]' => FALSE, + ], 'Save settings'); + + // Set prefixes to en and fr. + $this->drupalGet('/admin/config/regional/language/detection/url'); + $this->submitForm([ + 'prefix[en]' => 'en', + 'prefix[fr]' => 'fr', + ], 'Save configuration'); + + // Create a node type. + $this->drupalCreateContentType([ + 'type' => 'page', + 'name' => 'Basic page', + ]); + $this->node = $this->drupalCreateNode([ + 'type' => 'page', + 'title' => 'New page', + ]); + + $this->drupalGet('/admin/config/regional/content-language'); + $edit = [ + 'entity_types[node]' => TRUE, + 'settings[node][page][translatable]' => TRUE, + ]; + $this->submitForm($edit, 'Save configuration'); + + $local_tasks_block = $this->drupalPlaceBlock('local_tasks_block'); + $page_title_block = $this->drupalPlaceBlock('page_title_block'); + $this->blockId['local_tasks_block'] = $local_tasks_block->id(); + $this->blockId['page_title_block'] = $page_title_block->id(); + + $this->drupalGet('/node/' . $this->node->id() . '/translations/add/en/fr'); + $edit = [ + 'title[0][value]' => 'Nouvelle page', + ]; + $this->submitForm($edit, 'Save (this translation)'); + } + + /** + * Tests administration pages language. + */ + public function testLanguageAdministrationPagesLanguage(): void { + $local_tasks_block_prefix = '//div[contains(@data-contextual-id, "block:block=' . $this->blockId['local_tasks_block']; + $page_title_block_prefix = '//div[contains(@data-contextual-id, "block:block=' . $this->blockId['page_title_block']; + + $this->drupalGet('/node/' . $this->node->id()); + $this->assertSession()->elementsCount('xpath', $local_tasks_block_prefix . ':langcode=en&admin_langcode=en")]', 1); + $this->assertSession()->elementsCount('xpath', $page_title_block_prefix . ':langcode=en&admin_langcode=en")]', 1); + + $this->drupalGet('/fr/node/' . $this->node->id()); + $this->assertSession()->elementsCount('xpath', $local_tasks_block_prefix . ':langcode=fr&admin_langcode=en")]', 1); + $this->assertSession()->elementsCount('xpath', $page_title_block_prefix . ':langcode=fr&admin_langcode=en")]', 1); + + $this->drupalGet('/user/' . $this->admin->id() . '/edit'); + $this->submitForm(['preferred_admin_langcode' => 'fr'], 'Save'); + // drupal_flush_all_caches($this->kernel);. + $this->drupalGet('/node/' . $this->node->id()); + $this->assertSession()->elementsCount('xpath', $local_tasks_block_prefix . ':langcode=en&admin_langcode=fr")]', 1); + $this->assertSession()->elementsCount('xpath', $page_title_block_prefix . ':langcode=en&admin_langcode=fr")]', 1); + + $this->drupalGet('/fr/node/' . $this->node->id()); + $this->assertSession()->elementsCount('xpath', $local_tasks_block_prefix . ':langcode=fr&admin_langcode=fr")]', 1); + $this->assertSession()->elementsCount('xpath', $page_title_block_prefix . ':langcode=fr&admin_langcode=fr")]', 1); + } + +} diff --git a/core/modules/contextual/tests/src/FunctionalJavascript/ContextualTranslationTest.php b/core/modules/contextual/tests/src/FunctionalJavascript/ContextualTranslationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..675d176aab4b5bd7cf16f550d9b0d7092fd1beff --- /dev/null +++ b/core/modules/contextual/tests/src/FunctionalJavascript/ContextualTranslationTest.php @@ -0,0 +1,124 @@ +languageManager = $this->container->get('language_manager'); + $this->localeStorage = $this->container->get('locale.storage'); + + $this->drupalPlaceBlock('local_actions_block'); + $this->drupalPlaceBlock('local_tasks_block'); + + $this->adminUser = $this->createUser([], NULL, TRUE); + $this->drupalLogin($this->adminUser); + + $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']); + + ConfigurableLanguage::createFromLangcode('nl')->save(); + $this->rebuildContainer(); + + // Enable the 'Account administration pages' language detection. + $this->drupalGet('admin/config/regional/language/detection'); + $this->submitForm(['language_interface[enabled][language-user-admin]' => TRUE], 'Save settings'); + } + + /** + * Tests that contextual links are shown in the preferred admin language. + */ + public function testContextualLinksPreferredAdminLanguage(): void { + // Create a node and visit the translated page so new translation labels + // are added. + $nl_language = $this->languageManager->getLanguage('nl'); + $node1 = $this->drupalCreateNode(['type' => 'page']); + $this->drupalGet($node1->toUrl('canonical', ['language' => $nl_language])); + + // Add a translation for the 'Edit' string. + $edit_translation = $this->randomMachineName(); + $this->drupalGet('admin/config/regional/translate'); + $this->submitForm(['string' => 'Edit', 'langcode' => 'nl'], 'Filter'); + $textarea = current($this->xpath('//textarea')); + $lid = (string) $textarea->getAttribute('name'); + $this->submitForm([$lid => $edit_translation], 'Save translations'); + + // Configure a preferred admin language. + $this->adminUser->set('preferred_admin_langcode', 'nl'); + $this->adminUser->save(); + + // The edit link text should be using the translated string. + $this->drupalGet($node1->toUrl('canonical')); + $this->clickContextualLink('article', $edit_translation); + $this->assertSession()->addressEquals($node1->toUrl('edit-form')); + + // Change the preferred admin language. + $this->adminUser->set('preferred_admin_langcode', 'en'); + $this->adminUser->save(); + + // The edit link text should be using the english string. + $this->drupalGet($node1->toUrl('canonical')); + $this->clickContextualLink('article', 'Edit'); + $this->assertSession()->addressEquals($node1->toUrl('edit-form')); + } + +} diff --git a/core/modules/contextual/tests/src/Kernel/ContextualUnitTest.php b/core/modules/contextual/tests/src/Kernel/ContextualUnitTest.php index 5258ceb0c8a9f7312e187cae14c953eb2de9b9cc..db4e0a2a031011d3ae37fedeb81d6676b0913df4 100644 --- a/core/modules/contextual/tests/src/Kernel/ContextualUnitTest.php +++ b/core/modules/contextual/tests/src/Kernel/ContextualUnitTest.php @@ -38,10 +38,10 @@ public static function contextualLinksDataProvider(): array { 'route_parameters' => [ 'node' => '14031991', ], - 'metadata' => ['langcode' => 'en'], + 'metadata' => ['langcode' => 'en', 'admin_langcode' => 'en'], ], ], - 'node:node=14031991:langcode=en', + 'node:node=14031991:langcode=en&admin_langcode=en', 'olivero', ]; @@ -53,10 +53,10 @@ public static function contextualLinksDataProvider(): array { 'key' => 'baz', 1 => 'qux', ], - 'metadata' => ['langcode' => 'en'], + 'metadata' => ['langcode' => 'en', 'admin_langcode' => 'en'], ], ], - 'foo:0=bar&key=baz&1=qux:langcode=en', + 'foo:0=bar&key=baz&1=qux:langcode=en&admin_langcode=en', 'claro', ]; @@ -70,10 +70,11 @@ public static function contextualLinksDataProvider(): array { 'location' => 'page', 'display' => 'page_1', 'langcode' => 'en', + 'admin_langcode' => 'en', ], ], ], - 'views_ui_edit:view=frontpage:location=page&display=page_1&langcode=en', + 'views_ui_edit:view=frontpage:location=page&display=page_1&langcode=en&admin_langcode=en', 'olivero', ]; @@ -83,7 +84,7 @@ public static function contextualLinksDataProvider(): array { 'route_parameters' => [ 'node' => '14031991', ], - 'metadata' => ['langcode' => 'en'], + 'metadata' => ['langcode' => 'en', 'admin_langcode' => 'en'], ], 'foo' => [ 'route_parameters' => [ @@ -91,14 +92,14 @@ public static function contextualLinksDataProvider(): array { 'key' => 'baz', 1 => 'qux', ], - 'metadata' => ['langcode' => 'en'], + 'metadata' => ['langcode' => 'en', 'admin_langcode' => 'en'], ], 'edge' => [ 'route_parameters' => ['20011988'], - 'metadata' => ['langcode' => 'en'], + 'metadata' => ['langcode' => 'en', 'admin_langcode' => 'en'], ], ], - 'node:node=14031991:langcode=en|foo:0=bar&key=baz&1=qux:langcode=en|edge:0=20011988:langcode=en', + 'node:node=14031991:langcode=en&admin_langcode=en|foo:0=bar&key=baz&1=qux:langcode=en&admin_langcode=en|edge:0=20011988:langcode=en&admin_langcode=en', 'claro', ]; diff --git a/core/modules/language/language.services.yml b/core/modules/language/language.services.yml index 98184c214fe728ad7354d8127f07bbb2fdb09403..59f9b992b7e0aa467385df8f540025853fafbb61 100644 --- a/core/modules/language/language.services.yml +++ b/core/modules/language/language.services.yml @@ -29,3 +29,5 @@ services: arguments: ['@language_manager'] tags: - { name: paramconverter } + Drupal\language\AdminLanguageRender: + arguments: ['@language_manager', '@string_translation', '@current_user'] diff --git a/core/modules/language/src/AdminLanguageRender.php b/core/modules/language/src/AdminLanguageRender.php new file mode 100644 index 0000000000000000000000000000000000000000..680863026c676b4efcb212e76ae7eafc8ba1b261 --- /dev/null +++ b/core/modules/language/src/AdminLanguageRender.php @@ -0,0 +1,109 @@ +currentUser->getPreferredAdminLangcode(FALSE); + + if ($userAdminLangcode && ($this->currentUser->hasPermission('access administration pages') || $this->currentUser->hasPermission('view the administration theme'))) { + $element['#original_langcode'] = $this->languageManager->getCurrentLanguage()->getId(); + $this->languageManager->setCurrentLanguage($this->languageManager->getLanguage($userAdminLangcode)); + $this->translationManager->setDefaultLangcode($userAdminLangcode); + $this->languageManager->setConfigOverrideLanguage($this->languageManager->getLanguage($userAdminLangcode)); + } + + // Add the correct cache contexts in. + $metadata = CacheableMetadata::createFromRenderArray($element); + $metadata->addCacheContexts(['user.admin_language', 'user.permissions']); + $metadata->applyTo($element); + + return $element; + } + + /** + * Restore original language. + * + * @param \Drupal\Core\Render\Markup $content + * Rendered markup. + * @param array $element + * A renderable array. + * + * @return \Drupal\Core\Render\Markup + * Rendered markup. + */ + #[TrustedCallback] + public function restoreLanguage(Markup $content, array $element): Markup { + if (isset($element['#original_langcode'])) { + $langcode = $element['#original_langcode']; + $language = $this->languageManager->getLanguage($langcode); + $this->languageManager->setCurrentLanguage($language); + $this->translationManager->setDefaultLangcode($langcode); + $this->languageManager->setConfigOverrideLanguage($language); + } + + return $content; + } + +} diff --git a/core/modules/language/src/ConfigurableLanguageManager.php b/core/modules/language/src/ConfigurableLanguageManager.php index 61d883551f1f5bdea95c6b025a19d08fd1f6d05f..bf5a2a67801a64ef38b40838229ceed9c9dd8118 100644 --- a/core/modules/language/src/ConfigurableLanguageManager.php +++ b/core/modules/language/src/ConfigurableLanguageManager.php @@ -243,6 +243,13 @@ public function getCurrentLanguage($type = LanguageInterface::TYPE_INTERFACE) { return $this->negotiatedLanguages[$type]; } + /** + * {@inheritdoc} + */ + public function setCurrentLanguage(LanguageInterface $language, ?string $type = LanguageInterface::TYPE_INTERFACE): void { + $this->negotiatedLanguages[$type] = $language; + } + /** * {@inheritdoc} */ diff --git a/core/modules/language/src/Hook/LanguageHooks.php b/core/modules/language/src/Hook/LanguageHooks.php index edb93d49268bad1e8ed928d5c9c02ab52f7c2542..d779508b0c2b7f81e39bb48cef996734dbb36687 100644 --- a/core/modules/language/src/Hook/LanguageHooks.php +++ b/core/modules/language/src/Hook/LanguageHooks.php @@ -17,6 +17,7 @@ use Drupal\Core\Config\InstallStorage; use Drupal\Core\Config\FileStorage; use Drupal\Core\Installer\InstallerKernel; +use Drupal\language\AdminLanguageRender; use Drupal\language\Entity\ContentLanguageSettings; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Url; @@ -171,6 +172,14 @@ public function elementInfoAlter(&$type): void { $type['language_select']['#theme_wrappers'] = array_merge($type['language_select']['#theme_wrappers'], ['form_element']); $type['language_select']['#languages'] = LanguageInterface::STATE_CONFIGURABLE; $type['language_select']['#multiple'] = FALSE; + // Support rendering the links in the user's preferred admin language. + if (isset($type['toolbar'])) { + $type['toolbar'] = AdminLanguageRender::applyTo($type['toolbar']); + } + if (isset($type['contextual_links'])) { + $type['contextual_links'] = AdminLanguageRender::applyTo($type['contextual_links']); + } + } } diff --git a/core/modules/node/tests/src/Functional/Views/FrontPageTest.php b/core/modules/node/tests/src/Functional/Views/FrontPageTest.php index dab95d8b589f6b34863fcb7c5040c601b6092757..2b7c63581169223247bb6e2e6f5d767a4e51d9b5 100644 --- a/core/modules/node/tests/src/Functional/Views/FrontPageTest.php +++ b/core/modules/node/tests/src/Functional/Views/FrontPageTest.php @@ -321,6 +321,9 @@ protected function doTestFrontPageViewCacheTags($do_assert_views_caches): void { $do_assert_views_caches, $first_page_output_cache_tags ); + $cache_contexts = Cache::mergeContexts($cache_contexts, [ + 'user.admin_language', + ]); $this->assertPageCacheContextsAndTags( Url::fromRoute('view.frontpage.page_1'), $cache_contexts, diff --git a/core/modules/system/tests/src/Functional/System/ThemeTest.php b/core/modules/system/tests/src/Functional/System/ThemeTest.php index 1132ededea845f58de86da33e0a1c8f5da3b8754..33a5c42e29c5eab4474d47ef7a0215b937146d13 100644 --- a/core/modules/system/tests/src/Functional/System/ThemeTest.php +++ b/core/modules/system/tests/src/Functional/System/ThemeTest.php @@ -443,7 +443,7 @@ public function testSwitchDefaultTheme(): void { // Install Olivero and set it as the default theme. $theme_installer->install(['olivero']); $this->drupalGet('admin/appearance'); - $this->clickLink('Set as default'); + $this->click('A[title="Set Olivero as default theme"]'); $this->assertEquals('olivero', $this->config('system.theme')->get('default')); // Test the default theme on the secondary links (blocks admin page). @@ -453,7 +453,7 @@ public function testSwitchDefaultTheme(): void { // cleared. $this->drupalGet('admin/appearance'); // Stark is the first 'Set as default' link. - $this->clickLink('Set as default'); + $this->click('A[title="Set Stark as default theme"]'); $this->drupalGet('admin/structure/block'); $this->assertSession()->pageTextContains('Stark'); } diff --git a/core/modules/toolbar/tests/src/Functional/ToolbarMenuTranslationTest.php b/core/modules/toolbar/tests/src/Functional/ToolbarMenuTranslationTest.php index 7833619b031b595d77cf68143011e7d815ac3fda..3a8b13d948684948792f494251680e4a78a49862 100644 --- a/core/modules/toolbar/tests/src/Functional/ToolbarMenuTranslationTest.php +++ b/core/modules/toolbar/tests/src/Functional/ToolbarMenuTranslationTest.php @@ -72,49 +72,108 @@ public function testToolbarClasses(): void { // Visit a page that has the string on it so it can be translated. $this->drupalGet($langcode . '/admin/structure'); - // Search for the menu item. + // Check that the class is on the item before we translate it. + $this->assertSession()->elementsCount('xpath', '//a[contains(@class, "icon-system-admin-structure")]', 1); + + // Translate the menu item. + $menu_item_translated = $this->randomMachineName(); + $this->addLocalizedString($langcode, $menu_item, $menu_item_translated); + + // Go to another page in the custom language and make sure the menu item + // was translated. + $this->drupalGet($langcode . '/admin/structure'); + $this->assertSession()->pageTextContains($menu_item_translated); + + // Toolbar icons are included based on the presence of a specific class on + // the menu item. Ensure that class also exists for a translated menu item. + $xpath = $this->xpath('//a[contains(@class, "icon-system-admin-structure")]'); + $this->assertCount(1, $xpath, 'The menu item class is the same.'); + } + + /** + * Tests that the toolbar is shown in the preferred admin language. + */ + public function testToolbarRenderedInPreferredAdminLanguage(): void { + // Enable the 'Account administration pages' language detection. + $this->drupalGet('admin/config/regional/language/detection'); + $this->submitForm(['language_interface[enabled][language-user-admin]' => TRUE], 'Save settings'); + + $langcode = 'es'; + + // Add Spanish. + $this->drupalGet('admin/config/regional/language/add'); + $this->submitForm(['predefined_langcode' => $langcode], 'Add language'); + + // The menu item 'Structure' and 'View profile' in the toolbar will be + // translated. + $menu_item_structure = 'Structure'; + $menu_item_view_profile = 'View profile'; + + // Visit a page that has the string on it so it can be translated. + $this->drupalGet($langcode . '/admin/structure'); + $menu_item_structure_translated = $this->randomMachineName(); + $this->addLocalizedString($langcode, $menu_item_structure, $menu_item_structure_translated); + + // Add a translation for a menu item added using user_toolbar(). + $menu_item_view_profile_translated = $this->randomMachineName(); + $this->addLocalizedString($langcode, $menu_item_view_profile, $menu_item_view_profile_translated); + + // Go to another page in the custom language and make sure the menu item + // was translated. + $this->drupalGet($langcode . '/user'); + $this->assertSession()->elementContains('css', '#toolbar-link-system-admin_structure', $menu_item_structure_translated); + $this->assertSession()->elementContains('css', '#toolbar-item-user-tray a[title="User account"]', $menu_item_view_profile_translated); + + // Configure a preferred admin language. + $this->adminUser->set('preferred_admin_langcode', 'en'); + $this->adminUser->save(); + + drupal_flush_all_caches(); + + // Go to another page in the custom language and make sure the menu item + // is shown in the preferred admin language. + $this->drupalGet($langcode . '/user'); + $this->assertSession()->elementContains('css', '#toolbar-link-system-admin_structure', $menu_item_structure); + $this->assertSession()->elementContains('css', '#toolbar-item-user-tray a[title="User account"]', $menu_item_view_profile); + } + + /** + * Add a localized string. + * + * @param string $langcode + * The langcode. + * @param string $string + * The string to translate. + * @param string $translation + * The string translation. + */ + protected function addLocalizedString(string $langcode, string $string, string $translation): void { + // Search for the label. $search = [ - 'string' => $menu_item, + 'string' => $string, 'langcode' => $langcode, 'translation' => 'untranslated', ]; $this->drupalGet('admin/config/regional/translate'); $this->submitForm($search, 'Filter'); - // Make sure will be able to translate the menu item. + // Make sure will be able to translate the label. $this->assertSession()->pageTextNotContains('No strings available.'); - // Check that the class is on the item before we translate it. - $this->assertSession()->elementsCount('xpath', '//a[contains(@class, "icon-system-admin-structure")]', 1); + $textarea = current($this->xpath('//textarea')); - // Translate the menu item. - $menu_item_translated = $this->randomMachineName(); - $textarea = $this->assertSession()->elementExists('xpath', '//textarea'); $lid = (string) $textarea->getAttribute('name'); - $edit = [ - $lid => $menu_item_translated, - ]; - $this->drupalGet('admin/config/regional/translate'); - $this->submitForm($edit, 'Save translations'); + $this->submitForm([$lid => $translation], 'Save translations'); // Search for the translated menu item. $search = [ - 'string' => $menu_item, + 'string' => $string, 'langcode' => $langcode, 'translation' => 'translated', ]; $this->drupalGet('admin/config/regional/translate'); $this->submitForm($search, 'Filter'); // Make sure the menu item string was translated. - $this->assertSession()->pageTextContains($menu_item_translated); - - // Go to another page in the custom language and make sure the menu item - // was translated. - $this->drupalGet($langcode . '/admin/structure'); - $this->assertSession()->pageTextContains($menu_item_translated); - - // Toolbar icons are included based on the presence of a specific class on - // the menu item. Ensure that class also exists for a translated menu item. - $this->assertSession()->elementsCount('xpath', '//a[contains(@class, "icon-system-admin-structure")]', 1); + $this->assertSession()->pageTextContains($translation); } } diff --git a/core/modules/user/src/ToolbarLinkBuilder.php b/core/modules/user/src/ToolbarLinkBuilder.php index 8d989b339e5232f7c8fc2bd3975509671f19191c..9166d96a03dee8bda69f16754864afe1e879a2ba 100644 --- a/core/modules/user/src/ToolbarLinkBuilder.php +++ b/core/modules/user/src/ToolbarLinkBuilder.php @@ -2,10 +2,12 @@ namespace Drupal\user; +use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\Core\Session\AccountProxyInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Url; +use Drupal\language\AdminLanguageRender; /** * ToolbarLinkBuilder fills out the placeholders generated in user_toolbar(). @@ -14,21 +16,22 @@ class ToolbarLinkBuilder implements TrustedCallbackInterface { use StringTranslationTrait; - /** - * The current user. - * - * @var \Drupal\Core\Session\AccountProxyInterface - */ - protected $account; - /** * ToolbarHandler constructor. * * @param \Drupal\Core\Session\AccountProxyInterface $account * The current user. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler + * The module handler. */ - public function __construct(AccountProxyInterface $account) { - $this->account = $account; + public function __construct( + protected AccountProxyInterface $account, + protected ?ModuleHandlerInterface $moduleHandler = NULL, + ) { + if ($this->moduleHandler === NULL) { + @trigger_error('Calling ' . __METHOD__ . ' without the $moduleHandler argument is deprecated in drupal:11.4.0 and it will be required in drupal:12.0.0. See https://www.drupal.org/node/3455774', E_USER_DEPRECATED); + $this->moduleHandler = \Drupal::service('module_handler'); + } } /** @@ -69,6 +72,11 @@ public function renderToolbarLinks() { ], ]; + // Support rendering the links in the user's preferred admin language. + if ($this->moduleHandler->moduleExists('language')) { + $build = AdminLanguageRender::applyTo($build); + } + return $build; } diff --git a/core/modules/user/tests/src/Unit/ToolbarLinkBuilderTest.php b/core/modules/user/tests/src/Unit/ToolbarLinkBuilderTest.php index 96a22070f4951518c8219d1f181b77950a264474..d70e86398420bce067da27343f3a7a5bb99809e1 100644 --- a/core/modules/user/tests/src/Unit/ToolbarLinkBuilderTest.php +++ b/core/modules/user/tests/src/Unit/ToolbarLinkBuilderTest.php @@ -4,6 +4,7 @@ namespace Drupal\Tests\user\Unit; +use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Session\AccountProxyInterface; use Drupal\Tests\UnitTestCase; use Drupal\user\ToolbarLinkBuilder; @@ -24,7 +25,8 @@ public function testRenderDisplayName(): void { $account = $this->prophesize(AccountProxyInterface::class); $display_name = 'Something suspicious that should be #plain_text, not #markup'; $account->getDisplayName()->willReturn($display_name); - $toolbar_link_builder = new ToolbarLinkBuilder($account->reveal()); + $module_handler = $this->prophesize(ModuleHandlerInterface::class); + $toolbar_link_builder = new ToolbarLinkBuilder($account->reveal(), $module_handler->reveal()); $expected = ['#plain_text' => $display_name]; $this->assertSame($expected, $toolbar_link_builder->renderDisplayName()); } diff --git a/core/modules/user/user.services.yml b/core/modules/user/user.services.yml index 7cd90fb0e6bf97e9806815c0b4e9e9c92181f686..c204e646362b07b09f5deea8fe864f3f47391d9a 100644 --- a/core/modules/user/user.services.yml +++ b/core/modules/user/user.services.yml @@ -61,7 +61,7 @@ services: - { name: 'context_provider' } user.toolbar_link_builder: class: Drupal\user\ToolbarLinkBuilder - arguments: ['@current_user'] + arguments: ['@current_user', '@module_handler'] Drupal\user\ToolbarLinkBuilder: '@user.toolbar_link_builder' user.flood_control: class: Drupal\user\UserFloodControl diff --git a/core/modules/views_ui/tests/src/FunctionalJavascript/DisplayTest.php b/core/modules/views_ui/tests/src/FunctionalJavascript/DisplayTest.php index 838f4e46457f496c297252dc1206c0b1fbe8bf0c..4e5e5ad18fcb2325d1932087e6a150b48aac17fc 100644 --- a/core/modules/views_ui/tests/src/FunctionalJavascript/DisplayTest.php +++ b/core/modules/views_ui/tests/src/FunctionalJavascript/DisplayTest.php @@ -136,7 +136,7 @@ public function testPageContextualLinks(): void { $element = $this->getSession()->getPage()->find('css', $selector); $element->find('css', '.contextual button')->press(); - $contextual_container_id = 'entity.view.edit_form:view=test_display:location=page&name=test_display&display_id=page_1&langcode=en'; + $contextual_container_id = 'entity.view.edit_form:view=test_display:location=page&name=test_display&display_id=page_1&langcode=en&admin_langcode=en'; $contextual_container = $page->find('css', '[data-contextual-id="' . $contextual_container_id . '"]'); $this->assertNotEmpty($contextual_container);