How to show 404 page for nodes without translation? 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?
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(&amp;$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' =&gt; 'node_page_title',
    'title arguments' =&gt; 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' =&gt; 'node_page_view',
    'page arguments' =&gt; array(1),
    'access callback' =&gt; 'node_access',
    'access arguments' =&gt; 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-&gt;uid &amp;&amp; !isset($node-&gt;translations-&gt;data[$language-&gt;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-&gt;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-&gt;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-&gt;translations-&gt;data[$language-&gt;language])) {
    // Check if translation for fallback language exists
    if (LANGUAGE_NONE != $language-&gt;fallback &amp;&amp; isset($node-&gt;translations-&gt;data[$language-&gt;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(&amp;$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 = &amp;$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(&amp;$item, $map) {
  // Alter only custom menu items from "main menu"
  if (isset($item['module']) &amp;&amp; 'menu' == $item['module'] &amp;&amp; isset($item['menu_name']) &amp;&amp; '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-&gt;language) &amp;&amp; $entity-&gt;language == $language-&gt;language) {
    return true;
  }

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

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

  // Check if there is a fallback language translation available.
  if (LANGUAGE_NONE != $language-&gt;fallback &amp;&amp; !empty($entity-&gt;translations-&gt;data[$language-&gt;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 &amp;$links, $type, $path) {

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

  if (arg(0, $path) == 'node' &amp;&amp; arg(1, $path) &amp;&amp; !arg(2, $path)) {
    // !arg(2, $path) --&gt; 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-&gt;translations-&gt;data);
      $translations = array_flip($translations);
    }
  }

  $languages = language_list();

  foreach($links as $lang =&gt; &amp;$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]-&gt;fallback) | LANGUAGE_NONE != $languages[$lang]-&gt;fallback &amp;&amp; isset($translations[$languages[$lang]-&gt;fallback])) {
        continue;
      }
      $link['href'] = '&lt;front&gt;';
    }
  }
}