Overview

We have a very nice translation system in place that is based on translating fields on entities. For many cases this work very well, but in some cases we need to special care. One of these places are menu items. Right now, when you display menu items, fx with the menu block from core, all menu items are displayed regardless of it being translated or not. This is bad for a few reasons.

  • Having a menu with mixed languages is never desired for site builders / end users
  • It's not possible to hide menu items on certain languages (where they might not be relevant)

The ideal solution would only display menu items either without a specific language or in the current language.

Reproduce

  1. Install Drupal with several langauge and enable menu translation
  2. Display the main menu with the core menu block
  3. Create some pages with links in the main menu
  4. Translate some of the menu items

Expected result:
Only the translated menu items are displayed in the menu

Actual result:
All the menu items are displayed in the menu, untranslated in the original (or another) language.

Members fund testing for the Drupal project. Drupal Association Learn more

Comments

Gábor Hojtsy’s picture

Component: transliteration system » language system
Issue tags: +D8MI, +language-config, +language-ui

Having a menu with mixed languages is never desired for site builders / end users

I would say this is mostly not desired, but never is not really possible to say about a feature. Say your menu contains IKEA product names, they are not translated anywhere. ÄPPLARÖ, BEKANT, etc. will be the same regardless of site language. Even if you don't translate the items themselves, they may still lead to translated pages with product information in different languages.

As for how to approach this, menu items in core at least come from one of 3 places:

  • Menu items defined in core code. *.links.menu.yml
  • Menu items defined from views
  • Menu items defined as a menu link entity (either on the menu UI or on the node edit form)

The last one is the only case where we can explicitly tell whether the item has been translated to a language by looking at translation languages for that item. The items defined in views are translated in config translation. To be able to tell if you encountered the translated one or not, you need to reach back to loading the config translation for the view and see if the menu had a different textual value for the item. For *.links.menu.yml items, those are translated with the locale system and they may or may not have a translation there. Eg. 'Home', 'Create content', etc.

Edit: also note that contrib may source menu items from any other place, the system is pluggable. So each plugin need to decide for itself if the item is "translated" or not.

googletorp’s picture

Thanks for the details Gábor.

I don't want to start a discussion about never as I'm sure we can invent a case where a different behavior would be the desired result.
In the case of IKEA product names, I would think that the menu items should use one of the no language options, which we do want to show untranslated.

It should only be the menu items, which has a defined language that should be hidden if not translated. This only applies to menu link entities I believe, which makes this quite tricky, since menu items can come from a host of different places.

Gábor Hojtsy’s picture

In the case of IKEA product names, I would think that the menu items should use one of the no language options, which we do want to show untranslated.

Yeah so long as those are menu link entities as well, which are the only ones that have a language defined. Views or links.yml sourced items don't.

It should only be the menu items, which has a defined language that should be hidden if not translated. This only applies to menu link entities I believe, which makes this quite tricky, since menu items can come from a host of different places.

If you limit this to menu link entities (those are the only ones that have a defined language), then its definitely simpler. I would argue that people who define a page from views will have similar expectations about not showing if lack of translations and if they have a setting to not show untranslated items, then they will consider it a bug if only some items are hidden.

googletorp’s picture

It should only be the menu items, which has a defined language

This was a bit loose explained, maybe due to me not fully understanding the translation system. I was thinking that menus defined in yml, is defined in code and thus by default would be treated as being defined as english.

Maybe it would be a bit more clear to put it this way:

Menu items which has a language of "Not specified" or "Not applicable" should be displayed, regardless of which language is the active language

I still want to have a full solution for menu items coming from views and yml files.

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

matsbla’s picture

Priority: Normal » Major

I've tested this on Drupal 8.1.0 RC1 and seems like menu items are still present no matter if there exist a translation of the node it links to or not.

Is this not a major bug? I would say it is a really big drawback for multilingual site builders! Is there a work-around for this?

Also, when I try to translate the menu item in the node edit form it seems to be changed for all language (the Menu link title label seems to be not-translatable). I can also not find the menu items to be translated under configuration translation.

To me to seems like core functionalities for menu translation is not working.

matsbla’s picture

Category: Feature request » Bug report
badrange’s picture

This issue also affected us today. In our case, the menu items were translated, but the translations were not marked as published. Drupal chose to show the only published translation. This one goes into my D8 Gotcha list.. :)

matsbla’s picture

wow, thanks so much for that info, I've been struggling with the exact same issue myself, trying to understand why different menu items are displayed in different languages; some of them are published and not!

arskiainen’s picture

I think this could be pushed forward by at bare minimum exposing the menu link language to hook_menu_preprocess. For example adding something like:

  public function getLangcode() {
    return $this->getEntity()->language()->getId();
  }

in core/modules/menu_link_content/src/Plugin/Menu/MenuLinkContent.php. This is obviously a crude way to do it (and only works for the Custom menu link?), but allows for some control over the rendering of (un)translated menu items.

A similar approach would work for other menu items, if a standard way of exposing the language property would be implemented across the board. For the menu listing view also, exposing a filter for the language would be a nice to have feature.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

teroelonen’s picture

I agree with arskiainen on this. If the language of each menu link would be exposed on the hook_menu_preprocess we would have more control over the menu-items. This would solve issues I am having with the menu-items quite easily.

Xano’s picture

I cannot find any occurrence of hook_menu_preprocess(). Did you mean hook_preprocess_menu()? If so, I'd be wary about solving this problem in the theme layer.

I'd like to propose the introduction of a new PHP interface for menu link plugins, to expose their multilingual capabilities. Because the interface is optional (at least until Drupal 9), we will not break backwards compatibility. We can include an example implementation for links coming from menu link content entities, and create follow-up issues to implement similar functionality for menu links coming from other sources, such as Views.

svdhout’s picture

We have a use case with 15+ languages
We don't want to manage every menu separately for each language, but do want to allow some languages to have content in the menu that is not yet translated elsewhere.

Looks like the content language access module solves this issue for us.

epoitras’s picture

#14 worked for me. Thank you friend.

Note: Only works for internal paths.

KJankko’s picture

Is there any workaround to this problem? Translation system with menus is unusable at the moment if every node has not a translation.

I tested #14 but I think it is not a solution when using node translation (same node id for original node and translations).

I think every node translation should have unique menu item with language code. Then menu items could be easily filtered by language.

tuutti’s picture

Is there any workaround to this problem?

You can try https://www.drupal.org/sandbox/tuutti/2832329

It's not perfect, but has been enough for our needs so far.

vlad.dancer’s picture

@tuutti, Nice to see someone else working on workaround for this.
I'm also work on this, but we use different approach than overriding menu_tree manipulator service. We use hook_block_view_BASE_BLOCK_ID_alter() to filter menu items + add settings per block instance for multilingual things. Also we do override of MenuTreeStorage service to redefine plugin class for menu plugin to add getLanguage method.

Also I think that @arskiainen is right about getLanguage() method, or maybe change getEntity() method to public.

I will ask @matsbla to share this module.

vegardjo’s picture

Thank you @tuutti for this very timely lifesaver of a module! It works as advertised, only thing that stumped me for a bit is that it's not a drop-in replacement in the form that you current menu blocks will start working instantly after enabling the module, you need to place the menu block (of the new type) again. That is however fine as long as you know of it.

As for this entire issue I agree that this is quite a regression from d7 both for site builders and content producers. I've built numerous multi language Drupal sites and can't remember that the default behaviour in d8 core now is something I have ever wanted (not saying it *could* never happen). For site buildes it's a pain to maintain, place, configure and possible style one menu per language, and for the content producers they will not only need to translate the menu link, they also need to remember to switch to the new menu it should belong to. They will forget, again and again.

Gábor Hojtsy’s picture

@vegardjo: well, the goal with core was first and foremost to solve storing translations and bring contribs on the same page that ANYTHING on the system may be translated. The idea was/is that if we succeed with that then contribs can interoperate on top of a fully multilingual system which is going to be the biggest win for people. We could have focused on solving specific scenarios for multilingual sites, and miss some of the fundamental elements that are shared. Instead we focused on the fundamentals. I don't think this is a regression from Drupal 7 since Drupal 7 did not support menu translation at all. So that if you need to install a module to configure how the translated menu items are displayed that would be less of a hassle compared to what you needed to do in Drupal 7 and therefore still an improvement. Also once again, critically all other modules would be achieved that the menu is/may be translated unlike in Drupal 7 where i18n side-loaded the menu translations in some specific cases only.

vegardjo’s picture

You're right Gábor, it's wrong calling this a regression since d7 core offered little here. My bad. I am very aware of the huge improvements to all things language in d8.

That said, this still stands out like a major bug to me, and something that it seems to me should be handled by core. If I'm not very wrong everything else is always filtered by the current language in a multi language setup: all content, all strings, all blocks have a setting for it, views in core has a built in filter for it, and in general everything you see in the UI has this, just simply *not* the menu links, where the default behavior is plainly showing the original menu item for all languages, if not specifically translated. I find it very hard to think of a use case where this is what you actually want, and for me this isn't a very specific scenario, it's pretty much every scenario.

But to move forward, since you have the bigger picture: do you feel that this thing is something that is out of scope for core to solve? If so that's ok, and we'll just have make a note of it and wait for contrib to solve it.

tuutti’s picture

I'd like to propose the introduction of a new PHP interface for menu link plugins, to expose their multilingual capabilities.

Even if we decide not to include the actual filtering in core, this would help tremendously on implementing this in contrib land.

The patch contains a rough example of how this could be implemented for MenuLinkContent and how links could be filtered on a menu block level.

Gábor Hojtsy’s picture

Status: Active » Needs work
  1. +++ b/core/lib/Drupal/Core/Menu/LanguageMenuLinkManipulator.php
    @@ -0,0 +1,53 @@
    +  public function filterLanguage(array $tree) {
    +    $current_language = $this->languageManager->getCurrentLanguage()->getId();
    +
    +    foreach ($tree as $key => $link) {
    +      if (!$link->link instanceof MenuLinkTranslationInterface) {
    +        continue;
    +      }
    +      // Hide menu links that does not have translation for current language.
    +      if (!$link->link->hasTranslation($current_language)) {
    +        unset($tree[$key]);
    +      }
    +    }
    +    return $tree;
    +  }
    

    See the reason we are not doing this is that its not anywhere near clear that without making this configurable, this is something we should hardcode. If you want to put product/brand names in a menu (eg. you are selling watches or car tires), then those not showing up in the menu under certain languages pages is a bug. If you want to put the maintainers of a site section in a menu (users), not showing those because usernames are not translated would be a bug.

    So this points to the key reason at why we are not doing anything yet in core. We would need to figure out the ways core wants to support AND make it configurable so in cases where its a feature rather than a bug would be covered.

    It is true we are hiding content for things that are not translated in node views for example, but views are also configurable, so anyone can change the behavior.

  2. +++ b/core/modules/menu_link_content/src/Plugin/Menu/MenuLinkContent.php
    @@ -237,6 +238,16 @@ public function isTranslatable() {
    +    if (!$this->isTranslatable()) {
    +      return TRUE;
    +    }
    

    This would not satisfy the definition of the interface just how you personally intended to use it?

tuutti’s picture

The name of hasTranslation() does not really reflect what I intended it to do, but the idea behind hasTranslation() is that it should determine whether the menu link should be visible in the current language context.

For example Drupal\menu_link_content\Plugin\Menu\MenuLinkContent should be visible only if:

1) Menu link has translation for the current language
2) Menu link language is either 'Not specified' or 'Not applicable' (thus the isTranslatable() check)

and Drupal\views\Plugin\Menu\ViewsMenuLink should be visible if:

1) View has configuration override for given language (translation)
2) Some configuration tells to do otherwise (maybe Rendering Language setting?)

Other menu link types would obviously require an additional configuration, but the general idea is that we have an interface that determines if the menu link type is translatable and a method that determines if menu link has a translation and whether it should be visible or not.

Gábor Hojtsy’s picture

For example Drupal\menu_link_content\Plugin\Menu\MenuLinkContent should be visible only if:

1) Menu link has translation for the current language
2) Menu link language is either 'Not specified' or 'Not applicable' (thus the isTranslatable() check)

I don't think this is quite that simple. I knew clients where they did not have resources to translate everything 100% but wanted to display those parts of the site untranslated, so the experience is just degraded not cut off entirely. IMHO whatever solution we have in core needs to be configurable, which is primarily the reason we did not put any menu translation display solution in core, because it needs more consideration. If we put something in that is hardcoded, that still would be a regression compared to i18n in D7 which was configurable.

vlad.dancer’s picture

Hi, there.
I want share another solution for filtering the menu links using more filters.
https://www.drupal.org/sandbox/matsbla/2831709

provides multilingual features for menu blocks, to filter out menu items that do not have translated labels or link to untranslated content.

hkirsman’s picture

Tried both https://www.drupal.org/sandbox/tuutti/2832329 and https://www.drupal.org/sandbox/matsbla/2831709 but the firs is much better because the menu is clean also when logged in. Pretty much drop-in replacement - only thing to do is to replace the blocks. Nice job!

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

badrange’s picture

Thanks for making these sandbox projects, both look worth investigating! I wonder what the status is for @tuutti 's and @matsbla 's sandbox projects: Are they going to become full projects at some point in time or are you waiting for this issue to be resolved in Drupal core?

It is indeed easier to use full projects rather than sandbox modules in projects.

andypost’s picture

+++ b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php
@@ -151,6 +151,7 @@ public function build() {
     $manipulators = array(
       array('callable' => 'menu.default_tree_manipulators:checkAccess'),
       array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'),
+      array('callable' => 'menu.language_tree_manipulator:filterLanguage'),

This is the most pita here.
Every contrib that needs to add more manipulators require to override this block or implement own.

SO probably that need follow-up for make sort of hook for this place

vlad.dancer’s picture

I agree with @andypost. We should discuss how to expose altering of that thing.

tuutti’s picture

Thanks for making these sandbox projects, both look worth investigating! I wonder what the status is for @tuutti 's and @matsbla 's sandbox projects: Are they going to become full projects at some point in time or are you waiting for this issue to be resolved in Drupal core?

Probably not gonna be a full project anytime soon (unless someone else promotes it), because I'm too lazy to apply for a vetted git access.

hkirsman’s picture

@tuuti Would gladly help you with it. Sent a pm.

matsbla’s picture

tuutti opened one issue here about merging the modules, which I've been late to answer, but I finally did now;
https://www.drupal.org/node/2847313
maybe others want to give their feedbacks and ideas about what functionalities that are most useful to have in a contrib module

we also applied for full project here;
https://www.drupal.org/node/2835768

badrange’s picture

Thanks for the updates! I'll write a comment in #2847313.

michaelkoehne’s picture

I've created an recursive function for checking links in main menu, if they are translated into the current content language - if they're not, I remove them from output.

use Drupal\Core\Menu\MenuLinkInterface;
use Drupal\menu_link_content\Plugin\Menu\MenuLinkContent;
use Drupal\Core\Language\LanguageInterface;
/**
* Implements hook_preprocess_menu().
*/
function YOURMODULE_preprocess_menu(&$variables) {
  if ($variables['menu_name'] == 'main') {
    $language = Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId();
    foreach ($variables['items'] as $key => $item) {
      if(!$variables['items'][$key] = YOURMODULE_checkForMenuItemTranslation($item, $language)) {
        unset($variables['items'][$key]);
      }
    }
  }
}

function YOURMODULE_checkForMenuItemTranslation($item, $language) {
  $menuLinkEntity = YOURMODULE_load_link_entity_by_link($item['original_link']);

  if ($menuLinkEntity != NULL) {
    $languages = $menuLinkEntity->getTranslationLanguages();
    // Remove links which is not translated to current language.
    if (!array_key_exists($language, $languages)) {
      return false;
    } else {
      if(count($item['below']) > 0) {
        foreach ($item['below'] as $subkey => $subitem) {
          if(!$item['below'][$subkey] = YOURMODULE_checkForMenuItemTranslation($subitem, $language)) {
            unset($item['below'][$subkey]);
          }
        }
      }
      return $item;
    }

  }
}

function YOURMODULE_load_link_entity_by_link(MenuLinkInterface $menuLinkContentPlugin) {
  $entity = NULL;
  if ($menuLinkContentPlugin instanceof MenuLinkContent) {
    list($entity_type, $uuid) = explode(':', $menuLinkContentPlugin->getPluginId(), 2);
    $entity = \Drupal::entityManager()->loadEntityByUuid($entity_type, $uuid);
  }
  return $entity;
}
rwam’s picture

I've tried Menu Multilingual and it works fine for me. But in my opinion this should merged into core respective it should be implemented into core functionality. There are different scenarios where this is useful and necessary like unpublished translations or language dependent contents. The latter is a requirement of about 80% of all multilingual websites I made in the past (no matter what CMS). And it's a requirement of the current D8 project too. So I would say this functionality is mostly desired ;)

Gábor Hojtsy’s picture

I think the key problem is you would need to have some kind of per menu or per menu item (or content type or menu source) setting. Certain listing pages for example would make sense regardless of the listing page label translated, eg. if its "Microsoft", "Apple", "Samsung", etc. While others would not make sense. Also some people do want their content to not be partial even though it may not be fully translated. So I think while some simple cases are simple, the tricky part is figuring out the set of configuration options that cover an 80% use case AND not get in the way for someone who wants to do more advanced things. Which is basically why this is not *yet* in core.

TrevorBradley’s picture

I've got a use case to add to the pile.

My menu links to a view that displays videos. The view has a contextual filter that filters down by taxonomy attached to the video. e.g. /videos/1/64

We've just been asked to add a new category and a new video. The English video is ready, but the French is not. But the English video has to go up on our site ASAP.

I'd like to be able to disable the menu link to /fr/videos/1/64, but the English translation is showing up in my menu heirarchy. I'm not sure if I can change a menu or view setting that hides the English menu link on the French site, which goes to a view that displays no results.

Is there a setting or sandbox module that would work best for this? I was thinking of hacking it in with CSS - not the best solution.

alozie’s picture

Use case:
Current client wishes to exclude specific links created in master/default language from specific locales/translations.

Proposed solution:
Allow enable/disable menu item flag to be localized. Currently toggling off the enable flag eliminates menu item from default and all translations of a given menu. Instead allow translated items to be disabled. If translation exists and it is disabled the system should neither show it nor the default version, even if the default language version is flagged as enabled. Items without translations continue to show the default version. If a parent item of a menu tree is disabled for a given locale, neither it nor its children will display in the menu for that locale.

Proposed Implementation
The menu_link_content_data table contains a langcode and default_langcode field along with all the other information defining a menu item including the enabled field. Modify the menu rendering logic and the menu item translate form to accommodate the selective toggling of the enable field for specific menu items by the given language/locale.

Will follow with an attempt at a patch.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.6 was released on August 2, 2017 and is the final full bugfix release for the Drupal 8.3.x series. Drupal 8.3.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.4.0 on October 4, 2017. (Drupal 8.4.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.4.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.