2

I'm trying to add a language switcher to a Drupal 8 site menu (it's switching between English and Spanish). I have the links working when logged in as admin but they're not shown for anonymous users, even though the language switcher block displays fine, as well as adding a language switcher through twig. However, I'd really like to get this working and be able to add it to menus through a custom module.

As an anonymous user I am able to view the translated content (by clicking the Default Drupal language switcher options) but these links that are created are not rendered.

Here's my code for the custom module:

MYMODULE.links.menu.yml

MYMODULE.switch:
  class: Drupal\MYMODULE\Plugin\Menu\MenuLanguageLink
  menu_name: MENUNAME
  route_name: '<current>'
  options:
    attributes:
      hreflang: 'en'
MYMODULE.switch_2:
  class: Drupal\MYMODULE\Plugin\Menu\MenuLanguageLink
  menu_name: MENU2NAME
  route_name: '<current>'
  options:
    attributes:
      hreflang: 'es'

/src/Plugin/Menu/MenuLanguageLink.php:

<?php
/**
 * @file
 * Contains \Drupal\MYMODULE\Plugin\Menu\LanguageSwitchController.
 */
namespace Drupal\MYMODULE\Plugin\Menu;

use Drupal\Core\Menu\MenuLinkBase;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

class MenuLanguageLink extends MenuLinkBase implements ContainerFactoryPluginInterface {

  /** @var array Available languages */
  protected $languages = [];

  /**
   * {@inheritdoc}
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    LanguageManagerInterface $language_manager
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->languages = $language_manager->getLanguages();
  }

  /**
   * Dependency injection.
   */
  public static function create(
    ContainerInterface $container,
    array $configuration,
    $plugin_id,
    $plugin_definition
  ) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('language_manager')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function updateLink(array $new_definition_values, $persist) {
    return $this->pluginDefinition;
  }

  /**
   * {@inheritdoc}
   */
  public function getOptions() {
    $options = parent::getOptions();
    $langcode = $options['attributes']['hreflang'];

    if (isset($this->languages[$langcode])) {
      $options += ['language' => $this->languages[$langcode]];
    }

    return $options;
  }

  /**
   * {@inheritdoc}
   */
  public function getTitle() {
    $langcode = $this->getOptions()['attributes']['hreflang'];
    if ($langcode == 'es') {
      return 'Español';
    } else {
      return $this->languages[$langcode]->getName();
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getDescription() {
    return $this->t('Switch Language');
  }

}
4
  • Have you stripped the CSS & JS to make sure it's not being hidden and that is indeed not printed?
    – No Sssweat
    Dec 24, 2018 at 4:10
  • @NoSssweat Yes, there's no JS or CSS hiding the element, it's just not getting printed into the menu. Dec 24, 2018 at 21:35
  • Can you also check that it's not a permission issue? Does the anonymous role have the correct permissions?
    – No Sssweat
    Dec 24, 2018 at 22:02
  • 1
    @NoSssweat I can't find a permission that would need to be turned on (since I can access the content through the Drupal-provided language switcher block, as well as a simple twig-template switcher. However, I don't know if there are some permissions that are different for menu links over blocks - the content is definitely viewable by the anonymous user and can be accessed through those Drupal-provided methods. However, I can get the anonymous user to view the custom menu link when I give the permission to "View all content" which I obviously don't want to give to anon users. Dec 24, 2018 at 23:25

1 Answer 1

2

After installing your module, I can confirm the behavior that you're seeing.

When I change the route to <front> I am able to see the links as anonymous.

What I know is that the menu system automatically checks if the user has access or permission before it displays the links.

So my first thought is there must be a Core bug with this <current> route.

When I check /core/modules/system/system.routing.yml all I see is:

'<current>':
  path: '<current>'

Which is interesting that this route doesn't have an access nor a permission key.

In /admin/people/permissions if I check this permission:

Link to any page

Warning: Give to trusted roles only; this permission has security implications. This allows to bypass access checking when linking to internal paths.

I can see the links as anonymous, but obivously we do not want to this.


When I try to add <current> via the UI, I cannot; even though, <front> is acceptable.

enter image description here

Therefore, my best educated guess, seems to me that the <current> route is not designed to be used in Menu links because it doesn't have an access key nor permission key. Also, this might explain why the language switcher is a block and not a menu link.


However, there is work around for this if you pro-grammatically print the menu and not perform an access check, but there is a caveat. You would have to make sure there is no link in this menu that anonymous is not suppose to see.

As per code provided in Menu system, "Rendering" section, comment out the access check part.

Example:

    $menu_tree = \Drupal::menuTree();
    $menu_name = 'MENUNAME';

    // Build the typical default set of menu tree parameters.
    $parameters = $menu_tree
      ->getCurrentRouteMenuTreeParameters($menu_name);

    // Load the tree based on this set of parameters.
    $tree = $menu_tree
      ->load($menu_name, $parameters);

    // Transform the tree using the manipulators you want.
    $manipulators = array(
//    // Only show links that are accessible for the current user.
//    array(
//      'callable' => 'menu.default_tree_manipulators:checkAccess',
//    ),
      // Use the default sorting of menu links.
      array(
        'callable' => 'menu.default_tree_manipulators:generateIndexAndSort',
      ),
    );
    $tree = $menu_tree
      ->transform($tree, $manipulators);

    // Finally, build a renderable array from the transformed tree.
    $menu = $menu_tree
      ->build($tree);
    $menu_html = drupal_render($menu);

    print $menu_html; // print the menu

If the above does not suffice, because of the caveat, you might want to look into

Potentially write a custom menu tree manipulator, see \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators for examples. This is only necessary if you want to do things like adding extra metadata to rendered links to display icons next to them.

source: Menu System

I believe here you will be able to target specifically the language menu links and make Drupal skip the access check only on those specific language links which would be the best solution.

3
  • Would there be a way to create a MYMODULE.routing.yml file that provides the current path to the MYMODULE.links.menu.yml via a Controller? Dec 25, 2018 at 18:50
  • @KrisRobinson I don't see how. Plus the menu module does this automatically based on the route_name key that you provide in .links.menu.yml
    – No Sssweat
    Dec 26, 2018 at 0:00
  • Bummer. I think this solution is just getting a bit too hack-y. From _preprocess_page I can create a simple switcher for my page.html.twig so I guess for now I'll stick with that until I can find out more about <current> route permissions and why this behaves the way it does. Thank you so much for your time and pinpointing the issues here. Dec 26, 2018 at 0:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.