0

I'm using entity translation and I need to show 404 page for a node if it hasn't got a translation available for current language.

For example, the site runs EN, DE, FR, ES languages. Node 100 is translated to EN and DE. If i set current language to FR and visit the node 100, I'll see EN content due to the language fallback. But I would like to show 404 page.

How can I achieve this?

4 Answers 4

1

At first I was tempted to alter menu items responsible for node auto-loader function like this:

/**
 * Implements hook_menu_alter to hijack node auto-loader function.
 */
function et404_menu_alter(&$items) {

  // Remove original node view item...
  unset($items['node/%node']);

  // ... add our own node view item with custom auto-loader
  $items['node/%et404_hijacked_node'] = array(
    // Array items are copy-paste from node.module
    'title callback' => 'node_page_title',
    'title arguments' => array(1),
    // The page callback also invokes drupal_set_title() in case
    // the menu router's title is overridden by a menu link.
    'page callback' => 'node_page_view',
    'page arguments' => array(1),
    'access callback' => 'node_access',
    'access arguments' => array('view', 1),
  );
}

/**
 * Overrides node_load to show 404 page if translation is not available.
 */
function et404_hijacked_node_load($nid = NULL, $vid = NULL, $reset = FALSE) {
  global $user;
  global $language;

  $node = node_load($nid, $vid, $reset);

  // Only anonymous users are refused access
  if (!$user->uid && !isset($node->translations->data[$language->language])) {
    $node = false;
  }

  return $node;
}

Unfortunately this solution won't work correctly, because of menu_get_object() function that is used across drupal. That function assumes that every entity has it's auto-loader function named like ENTITY_TYPE_load(). The _load suffix is hardcoded into the function whereas it should be red from the entity info array provided by the hook_entity_info() (explicitly the load hook parameter).

I decided to use different approach that involves hook_node_access().

Actually instead of showing 404 with drupal_goto() I decided to incorporate drupal core and show 403. It can be turned into 404 anyway with a help of m4032404 module.

Please note that the following code is part of a module that also provides language fallback features similar to those from language fallback module - hence the reference to $language->fallback property that doesn't exists in vanilla D7. I am the author of language fallback patch for entity translation for D7 and D8 but for this particular project I needed something very sophisticated that I couldn't achieve with the language fallback module.

/**
 * Implements hook_node_access()
 */
function et404_node_access($node, $op, $account = null) {

  // We restrict access only to anonymous users so authenticated users can still manage content
  if ($account->uid) {
    return;
  }

  // Basic sanity check
  if (!is_object($node)) {
    return NODE_ACCESS_DENY;
  }

  global $language;

  // Check if translation for current language exists
  if (!isset($node->translations->data[$language->language])) {
    // Check if translation for fallback language exists
    if (LANGUAGE_NONE != $language->fallback && isset($node->translations->data[$language->fallback])) {
      return;
    }

    return NODE_ACCESS_DENY;
  }
}

/**
 * Implements hook_translated_menu_link_alter()
 *
 * This function checks if specific menu item points to the node entity
 * and if it is, checks if node has translation for current UI language.
 * Nodes without translation are hidden.
 *
 * See StackExchange answer for more info:
 * http://drupal.stackexchange.com/a/32024/5982
 */
function et404_translated_menu_link_alter(&$item, $map) {
  // Alter only custom menu items from "main menu"
  if ('menu' != $item['module'] || !isset($item['menu_name']) || 'main-menu' != $item['menu_name']) {
    return;
  }

  // Process only nodes
  if (!isset($map[1]) || !isset($map[0]) || 'node' != $map[0]) {
    return;
  }

  if(!is_object($map[1])) {
    // Instead of loading node manually, use core function for that.
    if (!_menu_load_objects($item, $map)) {
      return;
    }
  }

  // After _menu_load_objects() the $map[1] contains node object
  $entity = &$map[1];

  if (!et404_translation_check_entity($entity)) {
    // There is no translation for this entity in current UI language.
    // Hide the item and quit.
    $item['hidden'] = true;

    return;
  }
}

/**
 * Implements hook_menu_link_alter()
 *
 * When custom menu item is saved, we add 'alter' option to be able to
 * customize this item with hook_translated_menu_link_alter() implementation.
 * See et404_translated_menu_link_alter() for details.
 *
 * @param array $item
 * @param array $map
 */
function et404_menu_link_alter(&$item, $map) {
  // Alter only custom menu items from "main menu"
  if (isset($item['module']) && 'menu' == $item['module'] && isset($item['menu_name']) && 'main-menu' == $item['menu_name']) {
    $item['options']['alter'] = TRUE;
  }
}

/**
 * Check if there is a translation for entity in current UI language only
 * @param object $entity
 * @return boolean|string
 *   FALSE if translation is NOT available; otherwise true.
 *   Note that FALSE is returned also for entities without "translations" property.
 */
function et404_translation_check_entity($entity) {

  // Invalid data?
  if (empty($entity)) {
    return false;
  }

  global $language;

  // Check if current UI language is the same as entity language
  // This is a fail-safe in case "translations" property is missing
  // Note that taxonomy terms doesn't have language property
  if (!empty($entity->language) && $entity->language == $language->language) {
    return true;
  }

  // Check if "translations" property exists.
  // Without it further checks doesn't make sense
  if (empty($entity->translations)) {
    return false;
  }

  // Check if there is a current UI language translation available.
  if (!empty($entity->translations->data[$language->language])) {
    return true;
  }

  // Check if there is a fallback language translation available.
  if (LANGUAGE_NONE != $language->fallback && !empty($entity->translations->data[$language->fallback])) {
    return true;
  }

  // No translation available
  return false;
}

And as a bonus, a fix for language switching links:

/**
 * Implements hook_language_switch_links_alter
 *
 * Function alters language switching links for nodes
 * if there is no translation for currently viewed node
 */
function et404_language_switch_links_alter(array &$links, $type, $path) {

  $translations = array_keys($links);
  $translations = array_flip($translations);

  if (arg(0, $path) == 'node' && arg(1, $path) && !arg(2, $path)) {
    // !arg(2, $path) --> This will make sure we're not altering links like /node/1/edit/LANG

    // Most probably dealing with nodes
    $node = node_load(arg(1, $path));

    if ($node) {
      $translations = array_keys($node->translations->data);
      $translations = array_flip($translations);
    }
  }

  $languages = language_list();

  foreach($links as $lang => &$link) {
    // Check if translation for the language exists
    if (!isset($translations[$lang])) {
      // If not, check if the fallback translation is also missing.
      if (!isset($languages[$lang]->fallback) | LANGUAGE_NONE != $languages[$lang]->fallback && isset($translations[$languages[$lang]->fallback])) {
        continue;
      }
      $link['href'] = '<front>';
    }
  }
}
1

The first step in accomplishing a 404 page for missing translations for an entity/language combination is to disable the 'Enable language fallback' option found on the Entity Translation settings page (/admin/config/regional/entity_translation). This will prevent Drupal from displaying the source content, but it doesn't get us to the desired 404 and instead displays a '%language translation unavailable for %label.' message.

If we dig a little, we find that this message is provided by the 'entity_translation_unavailable' theme implementation. This means that we can override the theme implementation in our theme and call drupal_not_found() to return our desired 404. If we want to override this in a module, we can implement hook_theme_registry_alter() and specify a custom function for this theme implementation and call drupal_not_found() from that function.

0

I'm not aware of any contributed module that can be used for this. A possible solution could be to use a custom module that uses hook_node_view, as in this example:

function yourmodule_node_view($node, $view_mode, $langcode){
   global $language;
   if (!empty($node->language) && $node->language != $language->language) {
      drupal_not_found(); 
   }
}
0

In case you're using Entity Translation this is the simplest implementation you can do.

/**
 * Implements HOOH_node_view().
 *
 * @param $node
 * @param $view_mode
 * @param $langcode
 */
function HOOK_node_view($node, $view_mode, $langcode) {
  global $language, $user;

  // In case the translation is missing, trigger 404 for anonymous.
  if (!$user->uid && !isset($node->translations->data[$language->language])) {        
    drupal_not_found();
  }
}

Your Answer

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

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