diff --git modules/field/field.api.php modules/field/field.api.php
index 0f5a6a4..7fe8472 100644
--- modules/field/field.api.php
+++ modules/field/field.api.php
@@ -1043,12 +1043,44 @@ function hook_field_attach_delete_revision($entity_type, $entity) {
  *   - obj_type: The type of $entity; e.g. 'node' or 'user'.
  *   - object: The entity with fields to render.
  *   - view_mode: View mode, e.g. 'full', 'teaser'...
- *   - langcode: The language in which the field values will be displayed.
  */
 function hook_field_attach_view_alter(&$output, $context) {
 }
 
 /**
+ * Act on field_display_language().
+ *
+ * This hook is invoked to alter the array of display languages for the given
+ * entity.
+ *
+ * @param $display_language
+ *   A reference to an array of language codes keyed by field name.
+ * @param $context
+ *   An associative array containing:
+ *   - obj_type: The type of the entity to be displayed.
+ *   - object: The entity with fields to render.
+ *   - langcode: The language code $entity has to be displayed in.
+ */
+function hook_field_display_language_alter(&$fallback, $context) {
+}
+
+/**
+ * Act on field_multilingual_available_languages().
+ *
+ * This hook is invoked to alter the array of available languages for the given
+ * field.
+ *
+ * @param &$languages
+ *   A reference to an array of language codes to be made available.
+ * @param $context
+ *   An associative array containing:
+ *   - object_type: The type of the entity the field is attached to.
+ *   - field: A field data structure.
+ */
+function hook_field_languages_alter(&$languages, $context) {
+}
+
+/**
  * Act on field_attach_create_bundle.
  *
  * This hook is invoked after the field module has performed the operation.
diff --git modules/field/field.attach.inc modules/field/field.attach.inc
index 28f40b6..d77b6d9 100644
--- modules/field/field.attach.inc
+++ modules/field/field.attach.inc
@@ -168,6 +168,9 @@ define('FIELD_STORAGE_INSERT', 'insert');
  *  - 'deleted': If TRUE, the function will operate on deleted fields
  *    as well as non-deleted fields. If unset or FALSE, only
  *    non-deleted fields are operated on.
+ *  - 'language': A language code or an array of language codes keyed by field
+ *    name. It will be used to narrow down to a single value the available
+ *    languages to act on.
  */
 function _field_invoke($op, $entity_type, $entity, &$a = NULL, &$b = NULL, $options = array()) {
   // Merge default options.
@@ -196,10 +199,14 @@ function _field_invoke($op, $entity_type, $entity, &$a = NULL, &$b = NULL, $opti
     if ((!isset($options['field_id']) || $options['field_id'] == $instance['field_id']) && (!isset($options['field_name']) || $options['field_name'] == $field_name)) {
       $field = field_info_field($field_name);
       $field_translations = array();
-      $suggested_languages = empty($options['language']) ? NULL : array($options['language']);
+
+      // Unless a language suggestion is provided we iterate on all the
+      // available languages.
+      $available_languages = field_multilingual_available_languages($entity_type, $field);
+      $languages = _field_language_suggestion($available_languages, $options['language'], $field_name);
 
       // Initialize field translations according to the available languages.
-      foreach (field_multilingual_available_languages($entity_type, $field, $suggested_languages) as $langcode) {
+      foreach ($languages as $langcode) {
         $field_translations[$langcode] = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();
       }
 
@@ -271,6 +278,10 @@ function _field_invoke($op, $entity_type, $entity, &$a = NULL, &$b = NULL, $opti
  *  - 'deleted': If TRUE, the function will operate on deleted fields
  *    as well as non-deleted fields. If unset or FALSE, only
  *    non-deleted fields are operated on.
+ *  - 'language': A language code or an array of language codes keyed by field
+ *    name. It will be used to narrow down to a single value the available
+ *    languages to act on.
+ *
  * @return
  *   An array of returned values keyed by entity id.
  */
@@ -321,8 +332,11 @@ function _field_invoke_multiple($op, $entity_type, $entities, &$a = NULL, &$b =
         $grouped_entities[$field_id][$id] = $entities[$id];
         // Extract the field values into a separate variable, easily accessed
         // by hook implementations.
-        $suggested_languages = empty($options['language']) ? NULL : array($options['language']);
-        foreach (field_multilingual_available_languages($entity_type, $fields[$field_id], $suggested_languages) as $langcode) {
+        // Unless a language suggestion is provided we iterate on all the
+        // available languages.
+        $available_languages = field_multilingual_available_languages($entity_type, $fields[$field_id]);
+        $languages = _field_language_suggestion($available_languages, $options['language'], $field_name);
+        foreach ($languages as $langcode) {
           $grouped_items[$field_id][$langcode][$id] = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();
         }
       }
@@ -1200,9 +1214,12 @@ function field_attach_prepare_view($entity_type, $entities, $view_mode = 'full')
  *   A renderable array for the field values.
  */
 function field_attach_view($entity_type, $entity, $view_mode = 'full', $langcode = NULL) {
-  // Invoke field_default_view(). If no language is provided, use the current
-  // UI language.
-  $options = array('language' => field_multilingual_valid_language($langcode, FALSE));
+  // Determine the actual language to display for each field, given the
+  // languages available in the field data.
+  $display_language = field_display_language($entity_type, $entity, NULL, $langcode);
+  $options = array('language' => $display_language);
+
+  // Invoke field_default_view().
   $null = NULL;
   $output = _field_invoke_default('view', $entity_type, $entity, $view_mode, $null, $options);
 
@@ -1219,7 +1236,6 @@ function field_attach_view($entity_type, $entity, $view_mode = 'full', $langcode
     'obj_type' => $entity_type,
     'object' => $entity,
     'view_mode' => $view_mode,
-    'langcode' => $langcode,
   );
   drupal_alter('field_attach_view', $output, $context);
 
diff --git modules/field/field.module modules/field/field.module
index a5a5d12..b756b28 100644
--- modules/field/field.module
+++ modules/field/field.module
@@ -636,7 +636,10 @@ function field_view_field($entity_type, $entity, $field_name, $display = array()
 
     // Hook invocations are done through the _field_invoke() functions in
     // 'single field' mode, to reuse the language fallback logic.
-    $options = array('field_name' => $field_name, 'language' => field_multilingual_valid_language($langcode, FALSE));
+    // Determine the actual language to display for the field, given the
+    // languages available in the field data.
+    $display_language = field_display_language($entity_type, $entity, $field_name, $langcode);
+    $options = array('field_name' => $field_name, 'language' => $display_language);
     $null = NULL;
     list($id) = entity_extract_ids($entity_type, $entity);
 
@@ -647,13 +650,12 @@ function field_view_field($entity_type, $entity, $field_name, $display = array()
     // Build the renderable array.
     $result = _field_invoke_default('view', $entity_type, $entity, $display, $null, $options);
 
-    // Invoke hook_field_attach_view_alter() to tet other modules alter the
+    // Invoke hook_field_attach_view_alter() to let other modules alter the
     // renderable array, as in a full field_attach_view() execution.
     $context = array(
       'obj_type' => $entity_type,
       'object' => $entity,
       'view_mode' => '_custom',
-      'langcode' => $langcode,
     );
     drupal_alter('field_attach_view', $result, $context);
 
@@ -667,6 +669,27 @@ function field_view_field($entity_type, $entity, $field_name, $display = array()
 }
 
 /**
+ * Returns the field items in the language they currently would be displayed.
+ *
+ * @param $entity_type
+ *   The type of $entity.
+ * @param $entity
+ *   The entity containing the data to be displayed.
+ * @param $field_name
+ *   The field to be displayed.
+ * @param $langcode
+ *   Optional. The language code $entity->{$field_name} has to be displayed in.
+ *   If no value is given the current language will be used.
+ *
+ * @return
+ *   An array of field items keyed by delta if available, FALSE otherwise.
+ */
+function field_get_items($entity_type, $entity, $field_name, $langcode = NULL) {
+  $langcode = field_display_language($entity_type, $entity, $field_name, $langcode);
+  return isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : FALSE;
+}
+
+/**
  * Determine whether a field has any data.
  *
  * @param $field
diff --git modules/field/field.multilingual.inc modules/field/field.multilingual.inc
index d9f3d20..5e4200a 100644
--- modules/field/field.multilingual.inc
+++ modules/field/field.multilingual.inc
@@ -14,62 +14,81 @@ function field_multilingual_settings_changed() {
 }
 
 /**
- * Collect the available languages for the given entity type and field.
+ * Collects the available languages for the given entity type and field.
  *
- * If an entity has a translation handler and the given field is translatable,
- * a (not necessarily strict) subset of the current enabled languages will be
- * returned, otherwise only LANGUAGE_NONE will be returned. Since the
- * default value for a 'translatable' entity property is FALSE, we ensure that
- * only entities that are able to handle translations actually get translatable
- * fields.
+ * If the given field has language support enabled, an array of available
+ * languages will be returned, otherwise only LANGUAGE_NONE will be returned.
+ * Since the default value for a 'translatable' entity property is FALSE, we
+ * ensure that only entities that are able to handle translations actually get
+ * translatable fields.
  *
  * @param $entity_type
  *   The type of the entity the field is attached to, e.g. 'node' or 'user'.
  * @param $field
  *   A field structure.
- * @param $suggested_languages
- *   An array of language preferences which will be intersected with the enabled
- *   languages.
+ *
  * @return
  *   An array of valid language codes.
  */
-function field_multilingual_available_languages($entity_type, $field, $suggested_languages = NULL) {
+function field_multilingual_available_languages($entity_type, $field) {
   $field_languages = &drupal_static(__FUNCTION__, array());
   $field_name = $field['field_name'];
 
-  if (!isset($field_languages[$field_name]) || !empty($suggested_languages)) {
-    $translation_handlers = field_multilingual_check_translation_handlers($entity_type);
-
-    if ($translation_handlers && $field['translatable']) {
-      // The returned languages are a subset of the intersection of enabled ones
-      // and suggested ones.
-      $available_languages = field_multilingual_content_languages();
-      $languages = !empty($suggested_languages) ? $available_languages = array_intersect($available_languages, $suggested_languages) : $available_languages;
-
-      foreach (module_implements('field_languages') as $module) {
-        $function = $module . '_field_languages';
-        $function($entity_type, $field, $languages);
-      }
-      // Accept only available languages.
-      $result = array_values(array_intersect($available_languages, $languages));
-      // Do not cache suggested values as they might alter the general result.
-      if (empty($suggested_languages)) {
-        $field_languages[$field_name] = $result;
-      }
+  if (!isset($field_languages[$field_name])) {
+    // If the field has language support enabled we retrieve an (alterable) list
+    // of enabled languages, otherwise we return just LANGUAGE_NONE.
+    if (field_multilingual_enabled($entity_type, $field)) {
+      $languages = field_multilingual_content_languages();
+      // Let other modules alter the available languages.
+      $context = array('obj_type' => $entity_type, 'field' => $field);
+      drupal_alter('field_languages', $languages, $context);
+      $field_languages[$field_name] = $languages;
     }
     else {
-      $result = $field_languages[$field_name] = array(LANGUAGE_NONE);
+      $field_languages[$field_name] = array(LANGUAGE_NONE);
     }
   }
-  else {
-    $result = $field_languages[$field_name];
+
+  return $field_languages[$field_name];
+}
+
+/**
+ * Process the given language suggestion based on the available languages.
+ *
+ * If a non-empty language suggestion is provided it must appear among the
+ * available languages, otherwise it will be ignored.
+ *
+ * @param $available_languages
+ *   An array of valid language codes.
+ * @param $language_suggestion
+ *   A language code or an array of language codes keyed by field name.
+ * @param $field_name
+ *   The name of the field being processed.
+ *
+ * @return
+ *   An array of valid language codes.
+ */
+function _field_language_suggestion($available_languages, $language_suggestion, $field_name) {
+
+  // Handle possible language suggestions.
+  if (!empty($language_suggestion)) {
+    // We might have an array of language suggestions keyed by field name.
+    if (is_array($language_suggestion) && isset($language_suggestion[$field_name])) {
+      $language_suggestion = $language_suggestion[$field_name];
+    }
+
+    // If we have a language suggestion and the suggested language is available,
+    // we return only it.
+    if (in_array($language_suggestion, $available_languages)) {
+     $available_languages = array($language_suggestion);
+   }
   }
 
-  return $result;
+  return $available_languages;
 }
 
 /**
- * Return available content languages.
+ * Returns available content languages.
  *
  * The languages that may be associated to fields include LANGUAGE_NONE.
  *
@@ -77,11 +96,30 @@ function field_multilingual_available_languages($entity_type, $field, $suggested
  *   An array of language codes.
  */
 function field_multilingual_content_languages() {
-  return array_keys(language_list() + array(LANGUAGE_NONE => NULL));
+  $languages = language_list('enabled');
+  return array_keys($languages[1] + array(LANGUAGE_NONE => NULL));
+}
+
+/**
+ * Checks whether a field has language support.
+ *
+ * A field has language support enabled if its 'translatable' property is set to
+ * TRUE, and its object type has at least one translation handler registered.
+ *
+ * @param $entity_type
+ *   The type of the entity the field is attached to.
+ * @param $field
+ *   A field data structure.
+ *
+ * @return
+ *   TRUE if the field can be multilingual.
+ */
+function field_multilingual_enabled($entity_type, $field) {
+  return $field['translatable'] && field_multilingual_enabled_handler($entity_type);
 }
 
 /**
- * Check if a module is registered as a translation handler for a given entity.
+ * Checks if a module is registered as a translation handler for a given entity.
  *
  * If no handler is passed, simply check if there is any translation handler
  * enabled for the given entity type.
@@ -94,26 +132,34 @@ function field_multilingual_content_languages() {
  * @return
  *   TRUE, if the handler is allowed to manage field translations.
  */
-function field_multilingual_check_translation_handlers($entity_type, $handler = NULL) {
+function field_multilingual_enabled_handler($entity_type, $handler = NULL) {
   $entity_info = entity_get_info($entity_type);
 
   if (isset($handler)) {
     return isset($entity_info['translation'][$handler]) && !empty($entity_info['translation'][$handler]);
   }
   elseif (isset($entity_info['translation'])) {
-    foreach ($entity_info['translation'] as $handler_info) {
-      // The translation handler must use a non-empty data structure.
-      if (!empty($handler_info)) {
-        return TRUE;
+    $handler_info = &drupal_static(__FUNCTION__, array());
+
+    if (!isset($handler_info[$entity_type])) {
+      $handler_info[$entity_type] = FALSE;
+      foreach ($entity_info['translation'] as $info) {
+        // The translation handler must use a non-empty data structure.
+        if (!empty($info)) {
+          $handler_info[$entity_type] = TRUE;
+          break;
+        }
       }
     }
+
+    return $handler_info[$entity_type];
   }
 
   return FALSE;
 }
 
 /**
- * Helper function to ensure that a given language code is valid.
+ * Ensures that a given language code is valid.
  *
  * Checks whether the given language is one of the enabled languages. Otherwise,
  * it returns the current, global language; or the site's default language, if
@@ -133,10 +179,64 @@ function field_multilingual_valid_language($langcode, $default = TRUE) {
     return $langcode;
   }
   global $language;
-  $langcode = $default ? language_default('language') : $language->language;
-  if (in_array($langcode, $enabled_languages)) {
-    return $langcode;
+  return $default ? language_default('language') : $language->language;
+}
+
+/**
+ * Returns the display language for the fields attached to the given entity.
+ *
+ * The actual language for each given field is determined based on the requested
+ * language and the actual data available in the fields themselves.
+ * If there is no translation handler registered for the given entity type, the
+ * display language to be used is just LANGUAGE_NONE, as no other language code
+ * is allowed by field_multilingual_available_languages().
+ * If translation handlers are found, we let modules provide alternative display
+ * languages for fields not having the requested language available.
+ * Core language fallback rules are provided by locale_field_fallback() which is
+ * called by locale_field_display_language_alter().
+ *
+ * @param $entity_type
+ *   The type of $entity.
+ * @param $entity
+ *   The entity to be displayed.
+ * @param $field_name
+ *   Optional. If specified only the display language of the given field will be
+ *   returned.
+ * @param $langcode
+ *   Optional. The language code $entity has to be displayed in. If no value is
+ *   given the current language will be used.
+ *
+ * @return
+ *   A language code if a field name is specified, an array of language codes
+ *   keyed by field name otherwise.
+ */
+function field_display_language($entity_type, $entity, $field_name = NULL, $langcode = NULL) {
+  $display_languages = &drupal_static(__FUNCTION__, array());
+  list($id, , $bundle) = entity_extract_ids($entity_type, $entity);
+  $langcode = field_multilingual_valid_language($langcode, FALSE);
+
+  if (!isset($display_languages[$entity_type][$id][$langcode])) {
+    $display_language = array();
+
+    // By default display language is set to LANGUAGE_NONE. It is up to
+    // translation handlers to implement language fallback rules.
+    foreach (field_info_instances($entity_type, $bundle) as $instance) {
+      $display_language[$instance['field_name']] = LANGUAGE_NONE;
+    }
+
+    if (field_multilingual_enabled_handler($entity_type)) {
+      $context = array(
+        'obj_type' => $entity_type,
+        'object' => $entity,
+        'language' => $langcode,
+      );
+      drupal_alter('field_display_language', $display_language, $context);
+    }
+
+    $display_languages[$entity_type][$id][$langcode] = $display_language;
   }
-  // @todo Throw a more specific exception.
-  throw new FieldException('No valid content language could be found.');
+
+  $display_language = $display_languages[$entity_type][$id][$langcode];
+  $single_field = isset($field_name) && isset($display_language[$field_name]);
+  return $single_field ? $display_language[$field_name] : $display_language;
 }
diff --git modules/field/tests/field.test modules/field/tests/field.test
index e629dd7..80054dc 100644
--- modules/field/tests/field.test
+++ modules/field/tests/field.test
@@ -2548,9 +2548,6 @@ class FieldTranslationsTestCase extends FieldTestCase {
       'type' => 'test_field',
       'cardinality' => 4,
       'translatable' => TRUE,
-      'settings' => array(
-        'test_hook_in' => FALSE,
-      ),
     );
     field_create_field($field);
     $this->field = field_read_field($this->field_name);
@@ -2559,19 +2556,6 @@ class FieldTranslationsTestCase extends FieldTestCase {
       'field_name' => $this->field_name,
       'object_type' => $this->obj_type,
       'bundle' => 'test_bundle',
-      'label' => $this->randomName() . '_label',
-      'description' => $this->randomName() . '_description',
-      'weight' => mt_rand(0, 127),
-      'settings' => array(
-        'test_instance_setting' => $this->randomName(),
-      ),
-      'widget' => array(
-        'type' => 'test_field_widget',
-        'label' => 'Test Field',
-        'settings' => array(
-          'test_widget_setting' => $this->randomName(),
-        ),
-      ),
     );
     field_create_instance($instance);
     $this->instance = field_read_instance('test_entity', $this->field_name, 'test_bundle');
@@ -2582,64 +2566,35 @@ class FieldTranslationsTestCase extends FieldTestCase {
   }
 
   /**
-   * Ensure that only valid values are returned by field_multilingual_available_languages().
+   * Ensures that only valid values are returned by
+   * field_multilingual_available_languages().
    */
   function testFieldAvailableLanguages() {
     // Test 'translatable' fieldable info.
     field_test_entity_info_translatable('test_entity', FALSE);
     $field = $this->field;
     $field['field_name'] .= '_untranslatable';
-    $langcode = language_default();
-    $suggested_languages = array($langcode->language);
-    $available_languages = field_multilingual_available_languages($this->obj_type, $field, $suggested_languages);
-    $this->assertTrue(count($available_languages) == 1 && $available_languages[0] === LANGUAGE_NONE, t('Untranslatable entity: suggested language ignored.'));
 
     // Enable field translations for the entity.
     field_test_entity_info_translatable('test_entity', TRUE);
 
     // Test hook_field_languages() invocation on a translatable field.
-    $this->field['settings']['test_hook_in'] = TRUE;
-    $enabled_languages = array_keys(language_list());
+    variable_set('field_test_field_languages_alter', TRUE);
+    $enabled_languages = field_multilingual_content_languages();
     $available_languages = field_multilingual_available_languages($this->obj_type, $this->field);
-    $this->assertTrue(in_array(LANGUAGE_NONE, $available_languages), t('%language is an available language.', array('%language' => LANGUAGE_NONE)));
     foreach ($available_languages as $delta => $langcode) {
-      if ($langcode != LANGUAGE_NONE) {
+      if ($langcode != 'xx' && $langcode != 'en') {
         $this->assertTrue(in_array($langcode, $enabled_languages), t('%language is an enabled language.', array('%language' => $langcode)));
       }
     }
-    $this->assertFalse(in_array('xx', $available_languages), t('No invalid language was made available.'));
-    $this->assertTrue(count($available_languages) == count($enabled_languages), t('An enabled language was successfully made unavailable.'));
+    $this->assertTrue(in_array('xx', $available_languages), t('%language was made available.', array('%language' => 'xx')));
+    $this->assertFalse(in_array('en', $available_languages), t('%language was made unavailable.', array('%language' => 'en')));
 
     // Test field_multilingual_available_languages() behavior for untranslatable fields.
     $this->field['translatable'] = FALSE;
     $this->field_name = $this->field['field_name'] = $this->instance['field_name'] = drupal_strtolower($this->randomName() . '_field_name');
     $available_languages = field_multilingual_available_languages($this->obj_type, $this->field);
-    $this->assertTrue(count($available_languages) == 1 && $available_languages[0] === LANGUAGE_NONE, t('For untranslatable fields only neutral language is available.'));
-
-    // Test language suggestions.
-    $this->field['settings']['test_hook_in'] = FALSE;
-    $this->field['translatable'] = TRUE;
-    $this->field_name = $this->field['field_name'] = $this->instance['field_name'] = drupal_strtolower($this->randomName() . '_field_name');
-    $suggested_languages = array();
-    $lang_count = mt_rand(1, count($enabled_languages) - 1);
-    for ($i = 0; $i < $lang_count; ++$i) {
-      do {
-        $langcode = $enabled_languages[mt_rand(0, $lang_count)];
-      }
-      while (in_array($langcode, $suggested_languages));
-      $suggested_languages[] = $langcode;
-    }
-
-    $available_languages = field_multilingual_available_languages($this->obj_type, $this->field, $suggested_languages);
-    $this->assertEqual(count($available_languages), count($suggested_languages), t('Suggested languages were successfully made available.'));
-    foreach ($available_languages as $langcode) {
-      $this->assertTrue(in_array($langcode, $available_languages), t('Suggested language %language is available.', array('%language' => $langcode)));
-    }
-
-    $this->field_name = $this->field['field_name'] = $this->instance['field_name'] = drupal_strtolower($this->randomName() . '_field_name');
-    $suggested_languages = array('xx');
-    $available_languages = field_multilingual_available_languages($this->obj_type, $this->field, $suggested_languages);
-    $this->assertTrue(empty($available_languages), t('An invalid suggested language was not made available.'));
+    $this->assertTrue(count($available_languages) == 1 && $available_languages[0] === LANGUAGE_NONE, t('For untranslatable fields only LANGUAGE_NONE is available.'));
   }
 
   /**
@@ -2755,6 +2710,82 @@ class FieldTranslationsTestCase extends FieldTestCase {
       $this->assertTrue($result, t('%language translation correctly handled.', array('%language' => $langcode)));
     }
   }
+
+  /**
+   * Tests display language logic for translatable fields.
+   */
+  function testFieldDisplayLanguage() {
+    $field_name = drupal_strtolower($this->randomName() . '_field_name');
+    $entity_type = 'test_entity';
+
+    // We need an additional field here to properly test display language
+    // suggestions.
+    $field = array(
+      'field_name' => $field_name,
+      'type' => 'test_field',
+      'cardinality' => 2,
+      'translatable' => TRUE,
+    );
+    field_create_field($field);
+
+    $instance = array(
+      'field_name' => $field['field_name'],
+      'object_type' => $entity_type,
+      'bundle' => 'test_bundle',
+    );
+    field_create_instance($instance);
+
+    $entity = field_test_create_stub_entity(1, 1, $this->instance['bundle']);
+    list(, , $bundle) = entity_extract_ids($entity_type, $entity);
+    $instances = field_info_instances($entity_type, $bundle);
+
+    $enabled_languages = field_multilingual_content_languages();
+    $languages = array();
+
+    // Generate field translations for languages different from the first
+    // enabled.
+    foreach ($instances as $instance) {
+      $field_name = $instance['field_name'];
+      $field = field_info_field($field_name);
+      do {
+        // Index 0 is reserved for the requested language, this way we ensure
+        // that no field is actually populated with it.
+        $langcode = $enabled_languages[mt_rand(1, count($enabled_languages) - 1)];
+      }
+      while (isset($languages[$langcode]));
+      $languages[$langcode] = TRUE;
+      $entity->{$field_name}[$langcode] = $this->_generateTestFieldValues($field['cardinality']);
+    }
+
+    // Test multiple-fields display languages for untranslatable entities.
+    field_test_entity_info_translatable($entity_type, FALSE);
+    drupal_static_reset('field_display_language');
+    $requested_language = $enabled_languages[0];
+    $display_language = field_display_language($entity_type, $entity, NULL, $requested_language);
+    foreach ($instances as $instance) {
+      $field_name = $instance['field_name'];
+      $this->assertTrue($display_language[$field_name] == LANGUAGE_NONE, t('The display language for field %field_name is %language.', array('%field_name' => $field_name, '%language' => LANGUAGE_NONE)));
+    }
+
+    // Test multiple-fields display languages for translatable entities.
+    field_test_entity_info_translatable($entity_type, TRUE);
+    drupal_static_reset('field_display_language');
+    $display_language = field_display_language($entity_type, $entity, NULL, $requested_language);
+
+    foreach ($instances as $instance) {
+      $field_name = $instance['field_name'];
+      $langcode = $display_language[$field_name];
+      // As the requested language was not assinged to any field, if the
+      // returned language is defined for the current field, core fallback rules
+      // were successfully applied.
+      $this->assertTrue(isset($entity->{$field_name}[$langcode]) && $langcode != $requested_language, t('The display language for the field %field_name is %language.', array('%field_name' => $field_name, '%language' => $langcode)));
+    }
+
+    // Test single-field display language.
+    drupal_static_reset('field_display_language');
+    $langcode = field_display_language($entity_type, $entity, $this->field_name, $requested_language);
+    $this->assertTrue(isset($entity->{$this->field_name}[$langcode]) && $langcode != $requested_language, t('The display language for the (single) field %field_name is %language.', array('%field_name' => $field_name, '%language' => $langcode)));
+  }
 }
 
 /**
diff --git modules/field/tests/field_test.entity.inc modules/field/tests/field_test.entity.inc
index 069f0ad..77a28fc 100644
--- modules/field/tests/field_test.entity.inc
+++ modules/field/tests/field_test.entity.inc
@@ -64,6 +64,19 @@ function field_test_entity_info_alter(&$entity_info) {
 }
 
 /**
+ * Helper function to enable entity translations.
+ */
+function field_test_entity_info_translatable($entity_type = NULL, $translatable = NULL) {
+  drupal_static_reset('field_multilingual_enabled_handler');
+  $stored_value = &drupal_static(__FUNCTION__, array());
+  if (isset($entity_type)) {
+    $stored_value[$entity_type] = $translatable;
+    entity_info_cache_clear();
+  }
+  return $stored_value;
+}
+
+/**
  * Creates a new bundle for test_entity entities.
  *
  * @param $bundle
diff --git modules/field/tests/field_test.module modules/field/tests/field_test.module
index 07351fe..ce22b47 100644
--- modules/field/tests/field_test.module
+++ modules/field/tests/field_test.module
@@ -87,27 +87,24 @@ function field_test_field_test_op_multiple($entity_type, $entities, $field, $ins
 }
 
 /**
- * Implements hook_field_languages().
+ * Implements hook_field_languages_alter().
  */
-function field_test_field_languages($entity_type, $field, &$languages) {
-  if ($field['settings']['test_hook_in']) {
+function field_test_field_languages_alter(&$languages, $context) {
+  if (variable_get('field_test_field_languages_alter', FALSE)) {
     // Add an unavailable language.
     $languages[] = 'xx';
     // Remove an available language.
-    unset($languages[0]);
+    $index = array_search('en', $languages);
+    unset($languages[$index]);
   }
 }
 
 /**
- * Helper function to enable entity translations.
+ * Implements hook_field_display_language_alter().
  */
-function field_test_entity_info_translatable($entity_type = NULL, $translatable = NULL) {
-  $stored_value = &drupal_static(__FUNCTION__, array());
-  if (isset($entity_type)) {
-    $stored_value[$entity_type] = $translatable;
-    entity_info_cache_clear();
-  }
-  return $stored_value;
+function field_test_field_display_language_alter(&$display_language, $context) {
+  module_load_include('inc', 'locale', 'locale.field');
+  locale_field_fallback($display_language, $context['entity_type'], $context['entity'], $context['language']);
 }
 
 /**
diff --git modules/locale/locale.field.inc modules/locale/locale.field.inc
index 8cc340a..e69de29 100644
--- modules/locale/locale.field.inc
+++ modules/locale/locale.field.inc
@@ -1,77 +0,0 @@
-<?php
-// $Id: locale.field.inc,v 1.6 2009/12/26 16:50:09 dries Exp $
-
-/**
- * @file
- * Field API multilingual handling.
- */
-
-/**
- * Form submit handler for node_form().
- *
- * Update the field language according to the node language, changing the
- * previous language if necessary.
- */
-function locale_field_node_form_update_field_language($form, &$form_state, $reset_previous = TRUE) {
-  $node = (object) $form_state['values'];
-  $available_languages = field_multilingual_content_languages();
-  // @todo: Unify language neutral language codes.
-  $selected_language = empty($node->language) ? LANGUAGE_NONE : $node->language;
-  list(, , $bundle) = entity_extract_ids('node', $node);
-
-  foreach (field_info_instances('node', $bundle) as $instance) {
-    $field_name = $instance['field_name'];
-    $field = field_info_field($field_name);
-    $previous_language = $form[$field_name]['#language'];
-
-    // Handle a possible language change: previous language values are deleted,
-    // new ones are inserted.
-    if ($field['translatable'] && $previous_language != $selected_language) {
-      $form_state['values'][$field_name][$selected_language] = $node->{$field_name}[$previous_language];
-      if ($reset_previous) {
-        $form_state['values'][$field_name][$previous_language] = array();
-      }
-    }
-  }
-}
-
-/**
- * Apply fallback rules to the given object.
- *
- * Parameters are the same of hook_field_attach_view().
- */
-function locale_field_fallback_view(&$output, $context) {
-  // Lazily init fallback values and candidates to avoid unnecessary calls.
-  $fallback_values = array();
-  $fallback_candidates = NULL;
-  list(, , $bundle) = entity_extract_ids($context['obj_type'], $context['object']);
-
-  foreach (field_info_instances($context['obj_type'], $bundle) as $instance) {
-    $field_name = $instance['field_name'];
-    $field = field_info_field($field_name);
-
-    // If the items array is empty then we have a missing field translation.
-    // @todo: Verify this assumption.
-    if (isset($output[$field_name]) && count(element_children($output[$field_name])) == 0) {
-      if (!isset($fallback_candidates)) {
-        require_once DRUPAL_ROOT . '/includes/language.inc';
-        $fallback_candidates = language_fallback_get_candidates();
-      }
-
-      foreach ($fallback_candidates as $langcode) {
-        // Again if we have a non-empty array we assume the field translation is
-        // valid.
-        if (!empty($context['object']->{$field_name}[$langcode])) {
-          // Cache fallback values per language as fields might have different
-          // fallback values.
-          if (!isset($fallback_values[$langcode])) {
-            $fallback_values[$langcode] = field_attach_view($context['obj_type'], $context['object'], $context['view_mode'], $langcode);
-          }
-          // We are done, skip to the next field.
-          $output[$field_name] = $fallback_values[$langcode][$field_name];
-          break;
-        }
-      }
-    }
-  }
-}
diff --git modules/locale/locale.info modules/locale/locale.info
index aa3a03c..d8562b2 100644
--- modules/locale/locale.info
+++ modules/locale/locale.info
@@ -6,7 +6,6 @@ version = VERSION
 core = 7.x
 files[] = locale.module
 files[] = locale.install
-files[] = locale.field.inc
 files[] = locale.admin.inc
 files[] = locale.test
 configure = admin/config/regional/language
diff --git modules/locale/locale.install modules/locale/locale.install
index 04d0f67..4e96918 100644
--- modules/locale/locale.install
+++ modules/locale/locale.install
@@ -114,6 +114,7 @@ function locale_uninstall() {
   variable_del('locale_cache_strings');
   variable_del('locale_js_directory');
   variable_del('javascript_parsed');
+  variable_del('locale_field_display_language');
 
   foreach (language_types() as $type) {
     variable_del("language_negotiation_$type");
diff --git modules/locale/locale.module modules/locale/locale.module
index 2dbb3fc..952f9ac 100644
--- modules/locale/locale.module
+++ modules/locale/locale.module
@@ -419,13 +419,27 @@ function locale_form_alter(&$form, &$form_state, $form_id) {
 /**
  * Form submit handler for node_form().
  *
- * Check if Locale is registered as a translation handler and handle possible
+ * Checks if Locale is registered as a translation handler and handle possible
  * node language changes.
  */
 function locale_field_node_form_submit($form, &$form_state) {
-  if (field_multilingual_check_translation_handlers('node', 'locale')) {
-    module_load_include('inc', 'locale', 'locale.field');
-    locale_field_node_form_update_field_language($form, $form_state);
+  if (field_multilingual_enabled_handler('node', 'locale')) {
+    $node = (object) $form_state['values'];
+    $available_languages = field_multilingual_content_languages();
+    list(, , $bundle) = entity_extract_ids('node', $node);
+
+    foreach (field_info_instances('node', $bundle) as $instance) {
+      $field_name = $instance['field_name'];
+      $field = field_info_field($field_name);
+      $previous_language = $form[$field_name]['#language'];
+
+      // Handle a possible language change: new language values are inserted,
+      // previous ones are deleted.
+      if ($field['translatable'] && $previous_language != $node->language) {
+        $form_state['values'][$field_name][$node->language] = $node->{$field_name}[$previous_language];
+        $form_state['values'][$field_name][$previous_language] = array();
+      }
+    }
   }
 }
 
@@ -447,20 +461,60 @@ function locale_theme() {
 }
 
 /**
- * Implements hook_field_attach_view_alter().
+ * Implements hook_field_display_language_alter().
  */
-function locale_field_attach_view_alter(&$output, $context) {
-  // In locale_field_fallback_view() we might call field_attach_view(). The
-  // static variable avoids unnecessary recursion.
-  static $recursion;
-
-  // Do not apply fallback rules if disabled or if Locale is not registered as a
-  // translation handler.
-  if (!$recursion && variable_get('locale_field_fallback_view', TRUE) && field_multilingual_check_translation_handlers($context['obj_type'], 'locale')) {
-    $recursion = TRUE;
+function locale_field_display_language_alter(&$display_language, $context) {
+  // Do not apply core language fallback rules if they are disbaled or if Locale
+  // is not registered as a translation handler.
+  if (variable_get('locale_field_display_language', TRUE) && field_multilingual_enabled_handler($context['obj_type'], 'locale')) {
     module_load_include('inc', 'locale', 'locale.field');
-    locale_field_fallback_view($output, $context);
-    $recursion = FALSE;
+    locale_field_fallback($display_language, $context['obj_type'], $context['object'], $context['language']);
+  }
+}
+
+/**
+ * Applies fallback rules to the fields attached to the given object.
+ *
+ * Core language fallback rules simply check if fields have a field translation
+ * for the requested language code. If so the requested language is returned,
+ * otherwise all the fallback candidates are inspected to see if there is a
+ * field translation available in another language.
+ * By default this is called by locale_field_display_language_alter(), but this
+ * behavior can be disabled by setting the 'locale_field_display_language'
+ * variable to FALSE.
+ *
+ * @param $display_language
+ *   A reference to an array of language codes keyed by field name.
+ * @param $obj_type
+ *   The type of $object.
+ * @param $object
+ *   The object to be displayed.
+ * @param $langcode
+ *   Optional. The language code $object has to be displayed in.
+ */
+function locale_field_fallback(&$display_language, $obj_type, $object, $langcode) {
+  // Lazily init fallback candidates to avoid unnecessary calls.
+  $fallback_candidates = NULL;
+  $field_languages = array();
+
+  foreach ($display_language as $field_name => $field_language) {
+    // If the requested language is defined for the current field use it,
+    // otherwise search for a fallback value among the fallback candidates.
+    if (isset($object->{$field_name}[$langcode])) {
+      $display_language[$field_name] = $langcode;
+    }
+    elseif (!empty($object->{$field_name})) {
+      if (!isset($fallback_candidates)) {
+        require_once DRUPAL_ROOT . '/includes/language.inc';
+        $fallback_candidates = language_fallback_get_candidates();
+      }
+      foreach ($fallback_candidates as $fallback_language) {
+        if (isset($object->{$field_name}[$fallback_language])) {
+          $display_language[$field_name] = $fallback_language;
+          break;
+        }
+      }
+    }
   }
 }
 
