diff --git a/feeds.api.php b/feeds.api.php
index 9aee118..b40c310 100644
--- a/feeds.api.php
+++ b/feeds.api.php
@@ -459,10 +459,6 @@ function my_module_mapper_unique(FeedsSource $source, $entity_type, $bundle, $ta
 /**
  * Example of the preprocess_callbacks specified in hook_feeds_processor_targets().
  *
- * @param FeedsSource $source
- *   The Feed source.
- * @param object $entity
- *   The entity being processed.
  * @param array $target
  *   The full target definition.
  * @param array &$mapping
@@ -470,7 +466,7 @@ function my_module_mapper_unique(FeedsSource $source, $entity_type, $bundle, $ta
  *
  * @see hook_feeds_processor_targets()
  */
-function my_module_preprocess_callback(FeedsSource $source, $entity, array $target, array &$mapping) {
+function my_module_preprocess_callback(array $target, array &$mapping) {
   // Add in default values.
   $mapping += array('setting_value' => TRUE);
 }
diff --git a/feeds.info b/feeds.info
index c57e203..e543760 100644
--- a/feeds.info
+++ b/feeds.info
@@ -38,6 +38,7 @@ files[] = tests/feeds_mapper_field.test
 files[] = tests/feeds_mapper_file.test
 files[] = tests/feeds_mapper_hooks.test
 files[] = tests/feeds_mapper_link.test
+files[] = tests/feeds_mapper_multilingual_fields.test
 files[] = tests/feeds_mapper_path.test
 files[] = tests/feeds_mapper_profile.test
 files[] = tests/feeds_mapper_unique.test
diff --git a/mappers/date.inc b/mappers/date.inc
index d5ea81f..c75f125 100644
--- a/mappers/date.inc
+++ b/mappers/date.inc
@@ -37,7 +37,7 @@ function date_feeds_processor_targets($entity_type, $bundle_name) {
 /**
  * Callback for setting date values.
  */
-function date_feeds_set_target(FeedsSource $source, $entity, $target, array $values) {
+function date_feeds_set_target(FeedsSource $source, $entity, $target, array $values, array $mapping) {
   list($field_name, $sub_field) = explode(':', $target, 2);
 
   $delta = 0;
@@ -56,7 +56,7 @@ function date_feeds_set_target(FeedsSource $source, $entity, $target, array $val
       }
     }
 
-    $value->buildDateField($entity, $field_name, $delta);
+    $value->buildDateField($entity, $field_name, $delta, $mapping['language']);
     $delta++;
   }
 }
diff --git a/mappers/entity_translation.inc b/mappers/entity_translation.inc
new file mode 100644
index 0000000..cd6051b
--- /dev/null
+++ b/mappers/entity_translation.inc
@@ -0,0 +1,83 @@
+<?php
+
+/**
+ * @file
+ * On behalf implementation of Feeds mapping API for entity_translation.module.
+ */
+
+/**
+ * Implements hook_feeds_presave().
+ */
+function entity_translation_feeds_presave(FeedsSource $source, $entity, $item, $entity_id) {
+  $entity_type = $entity->feeds_item->entity_type;
+
+  // Check that it's a real entity type, and translation is enabled.
+  if (!entity_get_info($entity_type) || !entity_translation_enabled($entity_type, $entity)) {
+    return;
+  }
+
+  if (!$handler = entity_translation_get_handler($entity_type, $entity)) {
+    return;
+  }
+
+  list(, , $bundle) = entity_extract_ids($entity_type, $entity);
+
+  $languages_seen = array();
+
+  foreach (field_info_instances($entity_type, $bundle) as $instance) {
+    $field_name = $instance['field_name'];
+
+    // No values in this field, skip it.
+    if (empty($entity->$field_name) || !is_array($entity->$field_name)) {
+      continue;
+    }
+
+    // Not translatable.
+    $info = field_info_field($field_name);
+    if (!$info || !$info['translatable']) {
+      continue;
+    }
+
+    // Init the translation handler.
+    if (empty($handler->getTranslations()->original)) {
+      $handler->initTranslations();
+    }
+
+    // Avoid invalid user configuration. Entity translation does this when
+    // loading the translation overview page.
+    if (count($entity->$field_name) === 1 && key($entity->$field_name) === LANGUAGE_NONE && $handler->getLanguage() !== LANGUAGE_NONE) {
+      $entity->{$field_name}[$handler->getLanguage()] = $entity->{$field_name}[LANGUAGE_NONE];
+      $entity->{$field_name}[LANGUAGE_NONE] = array();
+    }
+
+    // Look for languages we haven't created a translation for yet.
+    foreach (array_diff_key($entity->$field_name, $languages_seen) as $language => $v) {
+      if ($language === LANGUAGE_NONE) {
+        continue;
+      }
+
+      $languages_seen[$language] = TRUE;
+
+      if ($language === $handler->getLanguage()) {
+        continue;
+      }
+
+      $translation = array(
+        'translate' => 0,
+        'status' => 1,
+        'language' => $language,
+        'source' => $handler->getLanguage(),
+      );
+
+      $handler->setTranslation($translation, $entity);
+    }
+  }
+
+  // Loop through every language for the site, and remove translations for the
+  // ones that don't have any values.
+  foreach (language_list() as $language) {
+    if (!isset($languages_seen[$language->language])) {
+      $handler->removeTranslation($language->language);
+    }
+  }
+}
diff --git a/mappers/file.inc b/mappers/file.inc
index 7fbfafe..644cbb2 100644
--- a/mappers/file.inc
+++ b/mappers/file.inc
@@ -59,7 +59,9 @@ function file_feeds_processor_targets($entity_type, $bundle_name) {
 /**
  * Callback for mapping file fields.
  */
-function file_feeds_set_target(FeedsSource $source, $entity, $target, array $values) {
+function file_feeds_set_target(FeedsSource $source, $entity, $target, array $values, array $mapping) {
+  $language = $mapping['language'];
+
   // Add default of uri for backwards compatibility.
   list($field_name, $sub_field) = explode(':', $target . ':uri');
   $info = field_info_field($field_name);
@@ -100,31 +102,31 @@ function file_feeds_set_target(FeedsSource $source, $entity, $target, array $val
   }
 
   // Populate entity.
-  $field = isset($entity->$field_name) ? $entity->$field_name : array(LANGUAGE_NONE => array());
+  $field = isset($entity->$field_name) ? $entity->$field_name : array($language => array());
   $delta = 0;
   foreach ($values as $v) {
     if ($info['cardinality'] == $delta) {
       break;
     }
 
-    if (!isset($field[LANGUAGE_NONE][$delta])) {
-      $field[LANGUAGE_NONE][$delta] = array();
+    if (!isset($field[$language][$delta])) {
+      $field[$language][$delta] = array();
     }
 
     switch ($sub_field) {
       case 'alt':
       case 'title':
       case 'description':
-        $field[LANGUAGE_NONE][$delta][$sub_field] = $v;
+        $field[$language][$delta][$sub_field] = $v;
         break;
 
       case 'uri':
         if ($v) {
           try {
             $v->setAllowedExtensions($instance_info['settings']['file_extensions']);
-            $field[LANGUAGE_NONE][$delta] += (array) $v->getFile($destination);
+            $field[$language][$delta] += (array) $v->getFile($destination);
             // @todo: Figure out how to properly populate this field.
-            $field[LANGUAGE_NONE][$delta]['display'] = 1;
+            $field[$language][$delta]['display'] = 1;
           }
           catch (Exception $e) {
             watchdog('feeds', check_plain($e->getMessage()));
diff --git a/mappers/link.inc b/mappers/link.inc
index e9c3632..1dc19dc 100644
--- a/mappers/link.inc
+++ b/mappers/link.inc
@@ -39,10 +39,12 @@ function link_feeds_processor_targets($entity_type, $bundle_name) {
 /**
  * Callback for mapping link fields.
  */
-function link_feeds_set_target(FeedsSource $source, $entity, $target, array $values) {
+function link_feeds_set_target(FeedsSource $source, $entity, $target, array $values, array $mapping) {
+  $language = $mapping['language'];
+
   list($field_name, $column) = explode(':', $target);
 
-  $field = isset($entity->$field_name) ? $entity->$field_name : array('und' => array());
+  $field = isset($entity->$field_name) ? $entity->$field_name : array($language => array());
   $delta = 0;
 
   foreach ($values as $value) {
@@ -51,7 +53,7 @@ function link_feeds_set_target(FeedsSource $source, $entity, $target, array $val
     }
 
     if (is_scalar($value)) {
-      $field['und'][$delta][$column] = (string) $value;
+      $field[$language][$delta][$column] = (string) $value;
     }
     $delta++;
   }
diff --git a/mappers/list.inc b/mappers/list.inc
index 8c5dc36..6a30829 100644
--- a/mappers/list.inc
+++ b/mappers/list.inc
@@ -49,8 +49,10 @@ function list_feeds_processor_targets($entity_type, $bundle_name) {
 /**
  * Callback for setting list_boolean fields.
  */
-function list_feeds_set_boolean_target(FeedsSource $source, $entity, $target, array $values) {
-  $field = isset($entity->$target) ? $entity->$target : array(LANGUAGE_NONE => array());
+function list_feeds_set_boolean_target(FeedsSource $source, $entity, $target, array $values, array $mapping) {
+  $language = $mapping['language'];
+
+  $field = isset($entity->$target) ? $entity->$target : array($language => array());
 
   foreach ($values as $value) {
 
@@ -58,7 +60,7 @@ function list_feeds_set_boolean_target(FeedsSource $source, $entity, $target, ar
       $value = $value->getValue();
     }
 
-    $field[LANGUAGE_NONE][] = array('value' => (int) (bool) $value);
+    $field[$language][] = array('value' => (int) (bool) $value);
   }
 
   $entity->$target = $field;
diff --git a/mappers/locale.inc b/mappers/locale.inc
new file mode 100644
index 0000000..5f235a1
--- /dev/null
+++ b/mappers/locale.inc
@@ -0,0 +1,118 @@
+<?php
+
+/**
+ * @file
+ * On behalf implementation of Feeds mapping API for locale.module.
+ */
+
+/**
+ * Implements hook_feeds_processor_targets_alter().
+ */
+function locale_feeds_processor_targets_alter(array &$targets, $entity_type, $bundle_name) {
+  foreach (array_keys($targets) as $target) {
+    $targets[$target]['preprocess_callbacks'][] = 'locale_feeds_preprocess_callback';
+    $targets[$target]['summary_callbacks'][] = 'locale_feeds_summary_callback';
+    $targets[$target]['form_callbacks'][] = 'locale_feeds_form_callback';
+  }
+}
+
+/**
+ * Preprocess callback that sets the configured mapping language.
+ */
+function locale_feeds_preprocess_callback(array $target, array &$mapping) {
+  if (empty($mapping['field_language'])) {
+    return;
+  }
+
+  $mapping['language'] = $mapping['field_language'];
+}
+
+/**
+ * Summary callback.
+ */
+function locale_feeds_summary_callback(array $mapping, array $target, array $form, array $form_state) {
+  $entity_type = $form_state['build_info']['args'][0]->processor->entityType();
+  $translatable = _locale_feeds_target_is_translatable($entity_type, $mapping['target']);
+
+  $mapping += array('field_language' => LANGUAGE_NONE);
+
+  $language_options = array(LANGUAGE_NONE => t('Language neutral')) + locale_language_list('name');
+
+  $error = NULL;
+  if ($mapping['field_language'] !== LANGUAGE_NONE && !$translatable) {
+    // This is an invalid configuration that can come from disabling
+    // entity_translation.
+    $error = t('Field not translatable');
+  }
+  if (!isset($language_options[$mapping['field_language']])) {
+    // This is an invalid configuration that can be caused by disabling or
+    // removing the language in question.
+    $error = t('Language \'@lang\' not available', array('@lang' => $mapping['field_language']));
+  }
+
+  // Nothing to see here.
+  if (!$error && !$translatable) {
+    return;
+  }
+
+  if ($error) {
+    return t('Language: <strong>Error: @error</strong>', array('@error' => $error));
+  }
+
+  return t('Language: %lang', array('%lang' => $language_options[$mapping['field_language']]));
+}
+
+/**
+ * Form callback.
+ */
+function locale_feeds_form_callback(array $mapping, array $target, array $form, array $form_state) {
+  $form = array();
+
+  $entity_type = $form_state['build_info']['args'][0]->processor->entityType();
+
+  $translatable = _locale_feeds_target_is_translatable($entity_type, $mapping['target']);
+  $mapping += array('field_language' => LANGUAGE_NONE);
+
+  // This is an invalid configuration that can come from disabling
+  // entity_translation.
+  $error = $mapping['field_language'] !== LANGUAGE_NONE && !$translatable;
+
+  // Nothing to see here.
+  if (!$error && !$translatable) {
+    return $form;
+  }
+
+  $language_options = array(LANGUAGE_NONE => t('Language neutral'));
+
+  if (!$error) {
+    $language_options += locale_language_list('name');
+  }
+
+  $form['field_language'] = array(
+    '#type' => 'select',
+    '#title' => t('Language'),
+    '#options' => $language_options,
+    '#default_value' => $mapping['field_language'],
+  );
+
+  return $form;
+}
+
+/**
+ * Determines if a target is translatable.
+ *
+ * @param string $entity_type
+ *   The entity type.
+ * @param string $target
+ *   The target.
+ *
+ * @return bool
+ *   Returns true if the target is translatable, false if not.
+ */
+function _locale_feeds_target_is_translatable($entity_type, $target) {
+  list($field_name) = explode(':', $target, 2);
+
+  $info = field_info_field($field_name);
+
+  return !empty($info) && field_is_translatable($entity_type, $info);
+}
diff --git a/mappers/number.inc b/mappers/number.inc
index 7f9a0d8..406b4f8 100644
--- a/mappers/number.inc
+++ b/mappers/number.inc
@@ -34,9 +34,11 @@ function number_feeds_processor_targets($entity_type, $bundle_name) {
 /**
  * Callback for mapping number fields.
  */
-function number_feeds_set_target(FeedsSource $source, $entity, $target, array $values) {
+function number_feeds_set_target(FeedsSource $source, $entity, $target, array $values, array $mapping) {
+  $language = $mapping['language'];
+
   // Iterate over all values.
-  $field = isset($entity->$target) ? $entity->$target : array('und' => array());
+  $field = isset($entity->$target) ? $entity->$target : array($language => array());
 
   foreach ($values as $value) {
 
@@ -45,7 +47,7 @@ function number_feeds_set_target(FeedsSource $source, $entity, $target, array $v
     }
 
     if (is_numeric($value)) {
-      $field['und'][] = array('value' => $value);
+      $field[$language][] = array('value' => $value);
     }
   }
 
diff --git a/mappers/taxonomy.inc b/mappers/taxonomy.inc
index 6fbdec6..a8a5006 100644
--- a/mappers/taxonomy.inc
+++ b/mappers/taxonomy.inc
@@ -85,6 +85,8 @@ function taxonomy_feeds_processor_targets($entity_type, $bundle_name) {
  * Callback for mapping taxonomy terms.
  */
 function taxonomy_feeds_set_target(FeedsSource $source, $entity, $target, array $terms, array $mapping) {
+  $language = $mapping['language'];
+
   // Add in default values.
   $mapping += array(
     'term_search' => FEEDS_TAXONOMY_SEARCH_TERM_NAME,
@@ -118,10 +120,14 @@ function taxonomy_feeds_set_target(FeedsSource $source, $entity, $target, array
     ->range(0, 1);
 
 
-  $field = isset($entity->$target) ? $entity->$target : array('und' => array());
+  $field = isset($entity->$target) ? $entity->$target : array($language => array());
+
+  if (!isset($field[$language])) {
+    $field[$language] = array();
+  }
 
   // Allow for multiple mappings to the same target.
-  $delta = count($field['und']);
+  $delta = count($field[$language]);
 
   // Iterate over all values.
   foreach ($terms as $term) {
@@ -159,6 +165,13 @@ function taxonomy_feeds_set_target(FeedsSource $source, $entity, $target, array
               'vid' => key($cache['allowed_vocabularies'][$target]),
               'vocabulary_machine_name' => reset($cache['allowed_vocabularies'][$target]),
             );
+            // Set language if the taxonomy is multilingual.
+            if ($language !== LANGUAGE_NONE) {
+              $info = entity_get_info('taxonomy_term');
+              if (!empty($info['entity keys']['language'])) {
+                $term->{$info['entity keys']['language']} = $language;
+              }
+            }
             taxonomy_term_save($term);
             $tid = $term->tid;
             // Add to the list of allowed values.
@@ -181,7 +194,7 @@ function taxonomy_feeds_set_target(FeedsSource $source, $entity, $target, array
     }
 
     if ($tid && isset($cache['allowed_values'][$target][$tid])) {
-      $field['und'][] = array('tid' => $tid);
+      $field[$language][] = array('tid' => $tid);
       $delta++;
     }
   }
diff --git a/mappers/text.inc b/mappers/text.inc
index aa9c2e1..1cf76bf 100644
--- a/mappers/text.inc
+++ b/mappers/text.inc
@@ -49,6 +49,8 @@ function text_feeds_processor_targets($entity_type, $bundle_name) {
  * Callback for mapping text fields.
  */
 function text_feeds_set_target(FeedsSource $source, $entity, $target, array $values, array $mapping) {
+  $language = $mapping['language'];
+
   list($field_name, $column) = explode(':', $target . ':value');
 
   if ($column === 'value' && isset($source->importer->processor->config['input_format'])) {
@@ -59,7 +61,7 @@ function text_feeds_set_target(FeedsSource $source, $entity, $target, array $val
     );
   }
 
-  $field = isset($entity->$field_name) ? $entity->$field_name : array('und' => array());
+  $field = isset($entity->$field_name) ? $entity->$field_name : array($language => array());
 
   // Iterate over all values.
   $delta = 0;
@@ -71,10 +73,10 @@ function text_feeds_set_target(FeedsSource $source, $entity, $target, array $val
 
     if (is_scalar($value) && strlen($value)) {
 
-      $field['und'][$delta][$column] = (string) $value;
+      $field[$language][$delta][$column] = (string) $value;
 
       if (isset($mapping['format'])) {
-        $field['und'][$delta]['format'] = $mapping['format'];
+        $field[$language][$delta]['format'] = $mapping['format'];
       }
     }
 
diff --git a/plugins/FeedsParser.inc b/plugins/FeedsParser.inc
index ad248ee..412888e 100644
--- a/plugins/FeedsParser.inc
+++ b/plugins/FeedsParser.inc
@@ -569,13 +569,13 @@ class FeedsDateTimeElement extends FeedsElement {
    * Helper method for buildDateField(). Build a FeedsDateTimeElement object
    * from a standard formatted node.
    */
-  protected static function readDateField($entity, $field_name, $delta = 0) {
+  protected static function readDateField($entity, $field_name, $delta = 0, $language = LANGUAGE_NONE) {
     $ret = new FeedsDateTimeElement();
-    if (isset($entity->{$field_name}['und'][$delta]['date']) && $entity->{$field_name}['und'][$delta]['date'] instanceof FeedsDateTime) {
-      $ret->start = $entity->{$field_name}['und'][$delta]['date'];
+    if (isset($entity->{$field_name}[$language][$delta]['date']) && $entity->{$field_name}[$language][$delta]['date'] instanceof FeedsDateTime) {
+      $ret->start = $entity->{$field_name}[$language][$delta]['date'];
     }
-    if (isset($entity->{$field_name}['und'][$delta]['date2']) && $entity->{$field_name}['und'][$delta]['date2'] instanceof FeedsDateTime) {
-      $ret->end = $entity->{$field_name}['und'][$delta]['date2'];
+    if (isset($entity->{$field_name}[$language][$delta]['date2']) && $entity->{$field_name}[$language][$delta]['date2'] instanceof FeedsDateTime) {
+      $ret->end = $entity->{$field_name}[$language][$delta]['date2'];
     }
     return $ret;
   }
@@ -590,10 +590,10 @@ class FeedsDateTimeElement extends FeedsElement {
    * @param int $delta
    *   The delta in the field.
    */
-  public function buildDateField($entity, $field_name, $delta = 0) {
+  public function buildDateField($entity, $field_name, $delta = 0, $language = LANGUAGE_NONE) {
     $info = field_info_field($field_name);
 
-    $oldfield = FeedsDateTimeElement::readDateField($entity, $field_name, $delta);
+    $oldfield = FeedsDateTimeElement::readDateField($entity, $field_name, $delta, $language);
     // Merge with any preexisting objects on the field; we take precedence.
     $oldfield = $this->merge($oldfield);
     $use_start = $oldfield->start;
@@ -626,27 +626,27 @@ class FeedsDateTimeElement extends FeedsElement {
 
     $db_tz = new DateTimeZone($db_tz);
     if (!isset($entity->{$field_name})) {
-      $entity->{$field_name} = array('und' => array());
+      $entity->{$field_name} = array($language => array());
     }
     if ($use_start) {
-      $entity->{$field_name}['und'][$delta]['timezone'] = $use_start->getTimezone()->getName();
-      $entity->{$field_name}['und'][$delta]['offset'] = $use_start->getOffset();
+      $entity->{$field_name}[$language][$delta]['timezone'] = $use_start->getTimezone()->getName();
+      $entity->{$field_name}[$language][$delta]['offset'] = $use_start->getOffset();
       $use_start->setTimezone($db_tz);
-      $entity->{$field_name}['und'][$delta]['date'] = $use_start;
+      $entity->{$field_name}[$language][$delta]['date'] = $use_start;
       /**
        * @todo the date_type_format line could be simplified based upon a patch
        *   DO issue #259308 could affect this, follow up on at some point.
        *   Without this, all granularity info is lost.
        *   $use_start->format(date_type_format($field['type'], $use_start->granularity));
        */
-      $entity->{$field_name}['und'][$delta]['value'] = $use_start->format(date_type_format($info['type']));
+      $entity->{$field_name}[$language][$delta]['value'] = $use_start->format(date_type_format($info['type']));
     }
     if ($use_end) {
       // Don't ever use end to set timezone (for now)
-      $entity->{$field_name}['und'][$delta]['offset2'] = $use_end->getOffset();
+      $entity->{$field_name}[$language][$delta]['offset2'] = $use_end->getOffset();
       $use_end->setTimezone($db_tz);
-      $entity->{$field_name}['und'][$delta]['date2'] = $use_end;
-      $entity->{$field_name}['und'][$delta]['value2'] = $use_end->format(date_type_format($info['type']));
+      $entity->{$field_name}[$language][$delta]['date2'] = $use_end;
+      $entity->{$field_name}[$language][$delta]['value2'] = $use_end->format(date_type_format($info['type']));
     }
   }
 }
diff --git a/plugins/FeedsProcessor.inc b/plugins/FeedsProcessor.inc
index 0644feb..d7102dc 100644
--- a/plugins/FeedsProcessor.inc
+++ b/plugins/FeedsProcessor.inc
@@ -696,6 +696,8 @@ abstract class FeedsProcessor extends FeedsPlugin {
    */
   protected function map(FeedsSource $source, FeedsParserResult $result, $target_item = NULL) {
     $targets = $this->getCachedTargets();
+    // Get fields for the entity type we are mapping to.
+    $fields = field_info_instances($this->entityType(), $this->bundle());
 
     if (empty($target_item)) {
       $target_item = array();
@@ -704,12 +706,24 @@ abstract class FeedsProcessor extends FeedsPlugin {
     // Many mappers add to existing fields rather than replacing them. Hence we
     // need to clear target elements of each item before mapping in case we are
     // mapping on a prepopulated item such as an existing node.
-    foreach ($this->config['mappings'] as $mapping) {
+    foreach ($this->getMappings() as $mapping) {
       if (isset($targets[$mapping['target']]['real_target'])) {
-        $target_item->{$targets[$mapping['target']]['real_target']} = NULL;
+        $target_name = $targets[$mapping['target']]['real_target'];
       }
       else {
-        $target_item->{$mapping['target']} = NULL;
+        $target_name = $mapping['target'];
+      }
+
+      // If the target is a field empty the value for the targeted language
+      // only.
+      // In all other cases, just empty the target completely.
+      if (isset($fields[$target_name])) {
+        // Empty the target for the specified language.
+        $target_item->{$target_name}[$mapping['language']] = array();
+      }
+      else {
+        // Empty the whole target.
+        $target_item->{$target_name} = NULL;
       }
     }
 
@@ -717,7 +731,7 @@ abstract class FeedsProcessor extends FeedsPlugin {
     // the parser's getSourceElement() method to retrieve the value of the
     // source element and pass it to the processor's setTargetElement() to stick
     // it on the right place of the target item.
-    foreach ($this->config['mappings'] as $mapping) {
+    foreach ($this->getMappings() as $mapping) {
       $value = $this->getSourceValue($source, $result, $mapping['source']);
 
       $this->mapToTarget($source, $mapping['target'], $target_item, $value, $mapping);
@@ -764,12 +778,6 @@ abstract class FeedsProcessor extends FeedsPlugin {
   protected function mapToTarget(FeedsSource $source, $target, &$target_item, $value, array $mapping) {
     $targets = $this->getCachedTargets();
 
-    if (isset($targets[$target]['preprocess_callbacks'])) {
-      foreach ($targets[$target]['preprocess_callbacks'] as $callback) {
-        call_user_func_array($callback, array($source, $target_item, $target, &$mapping));
-      }
-    }
-
     // Map the source element's value to the target.
     // If the mapping specifies a callback method, use the callback instead of
     // setTargetElement().
@@ -913,7 +921,38 @@ abstract class FeedsProcessor extends FeedsPlugin {
    * Get mappings.
    */
   public function getMappings() {
-    return isset($this->config['mappings']) ? $this->config['mappings'] : array();
+    $cache = &drupal_static('FeedsProcessor::getMappings', array());
+
+    if (!isset($cache[$this->id])) {
+      $mappings = $this->config['mappings'];
+      $targets = $this->getCachedTargets();
+      $languages = language_list('enabled');
+
+      foreach ($mappings as &$mapping) {
+
+        if (isset($targets[$mapping['target']]['preprocess_callbacks'])) {
+          foreach ($targets[$mapping['target']]['preprocess_callbacks'] as $callback) {
+            call_user_func_array($callback, array($targets[$mapping['target']], &$mapping));
+          }
+        }
+
+        // Ensure there's always a language set.
+        if (empty($mapping['language'])) {
+          $mapping['language'] = LANGUAGE_NONE;
+        }
+        else {
+          // Check if the configured language is available. If not, fallback to
+          // LANGUAGE_NONE.
+          if (!isset($languages[1][$mapping['language']])) {
+            $mapping['language'] = LANGUAGE_NONE;
+          }
+        }
+      }
+
+      $cache[$this->id] = $mappings;
+    }
+
+    return $cache[$this->id];
   }
 
   /**
@@ -1054,7 +1093,7 @@ abstract class FeedsProcessor extends FeedsPlugin {
   protected function uniqueTargets(FeedsSource $source, FeedsParserResult $result) {
     $parser = feeds_importer($this->id)->parser;
     $targets = array();
-    foreach ($this->config['mappings'] as $mapping) {
+    foreach ($this->getMappings() as $mapping) {
       if (!empty($mapping['unique'])) {
         // Invoke the parser's getSourceElement to retrieve the value for this
         // mapping's source.
@@ -1118,7 +1157,7 @@ abstract class FeedsProcessor extends FeedsPlugin {
   protected function hash($item) {
     $sources = feeds_importer($this->id)->parser->getMappingSourceList();
     $mapped_item = array_intersect_key($item, array_flip($sources));
-    return hash('md5', serialize($mapped_item) . serialize($this->config['mappings']));
+    return hash('md5', serialize($mapped_item) . serialize($this->getMappings()));
   }
 
   /**
diff --git a/tests/feeds/multilingual_empty.csv b/tests/feeds/multilingual_empty.csv
new file mode 100644
index 0000000..33e5706
--- /dev/null
+++ b/tests/feeds/multilingual_empty.csv
@@ -0,0 +1,2 @@
+"guid","title","body","date","datestamp","datetime","image","image_alt","image_title","link","number_decimal","number_float","number_integer","term","text"
+1,,,,,,,,,,,,,,
\ No newline at end of file
diff --git a/tests/feeds/multilingual_en_fr.csv b/tests/feeds/multilingual_en_fr.csv
new file mode 100644
index 0000000..8bf5b99
--- /dev/null
+++ b/tests/feeds/multilingual_en_fr.csv
@@ -0,0 +1 @@
+"guid","title_en","title_fr","body_en","body_fr","date_en","date_fr","datestamp_en","datestamp_fr","datetime_en","datetime_fr","image_en","image_fr","image_alt_en","image_alt_fr","image_title_en","image_title_fr","link_en","link_fr","number_decimal_en","number_decimal_fr","number_float_en","number_float_fr","number_integer_en","number_integer_fr","term_en","term_fr","text_en","text_fr"1,"Testing Multilingual Feeds 1","Teste Feeds Multilingue 1","This is the body","Ceci est la corps","21-10-2015","05-11-1955",1445470140,-446731187,"21-10-2015 23:29","1955-11-05 12:00:13","public://images/foosball.jpeg","public://images/la fayette.jpeg","Foosball","La Fayette","Foosball played by two guys","la Fayette dans les bois","http://google.ca","http://google.fr",4.2,1.2,3.1416,5.6295,1000,2000,"News","Nouvelles","Carrots","Carottes"
\ No newline at end of file
diff --git a/tests/feeds/multilingual_en_fr_empty.csv b/tests/feeds/multilingual_en_fr_empty.csv
new file mode 100644
index 0000000..223548e
--- /dev/null
+++ b/tests/feeds/multilingual_en_fr_empty.csv
@@ -0,0 +1 @@
+"guid","title_en","title_fr","body_en","body_fr","date_en","date_fr","datestamp_en","datestamp_fr","datetime_en","datetime_fr","image_en","image_fr","image_alt_en","image_alt_fr","image_title_en","image_title_fr","link_en","link_fr","number_decimal_en","number_decimal_fr","number_float_en","number_float_fr","number_integer_en","number_integer_fr","term_en","term_fr","text_en","text_fr"1,,"Teste Feeds Multilingue 1",,"Ceci est la corps",,"05-11-1955",,-446731187,,"1955-11-05 12:00:13",,"public://images/la fayette.jpeg",,"La Fayette",,"la Fayette dans les bois",,"http://google.fr",,1.2,,5.6295,,2000,,"Nouvelles",,"Carottes"
\ No newline at end of file
diff --git a/tests/feeds/multilingual_fr.csv b/tests/feeds/multilingual_fr.csv
new file mode 100644
index 0000000..7dc8e4d
--- /dev/null
+++ b/tests/feeds/multilingual_fr.csv
@@ -0,0 +1,2 @@
+"guid","title","body","date","datestamp","datetime","image","image_alt","image_title","link","number_decimal","number_float","number_integer","term","text"
+1,"Teste Feeds Multilingue 1","Ceci est la corps","05-11-1955",-446731187,"1955-11-05 12:00:13","public://images/la fayette.jpeg","La Fayette","la Fayette dans les bois","http://google.fr",1.2,5.6295,2000,"Nouvelles","Carottes"
\ No newline at end of file
diff --git a/tests/feeds/multilingual_nl.csv b/tests/feeds/multilingual_nl.csv
new file mode 100644
index 0000000..5680026
--- /dev/null
+++ b/tests/feeds/multilingual_nl.csv
@@ -0,0 +1,2 @@
+"guid","title","body","date","datestamp","datetime","image","image_alt","image_title","link","number_decimal","number_float","number_integer","term","text"
+1,"Feeds-meertaligheid 1 testen","Dit is de berichttekst","29-07-1985",491460492,"1985-07-29 4:48:12","public://images/attersee.jpeg","Bij het zien","Bij het zien van de groene vloeistof","http://google.nl",30.3,30.2795,30,"Nieuws","Wortelen"
\ No newline at end of file
diff --git a/tests/feeds_mapper_multilingual_fields.test b/tests/feeds_mapper_multilingual_fields.test
new file mode 100644
index 0000000..a54e979
--- /dev/null
+++ b/tests/feeds_mapper_multilingual_fields.test
@@ -0,0 +1,1032 @@
+<?php
+
+/**
+ * @file
+ * Contains FeedsMapperMultilingualFieldsTestCase.
+ */
+
+/**
+ * Tests field mapping with multiple languages.
+ */
+class FeedsMapperMultilingualFieldsTestCase extends FeedsMapperTestCase {
+
+  /**
+   * Name of created content type.
+   *
+   * @var string
+   */
+  private $contentType;
+
+  /**
+   * @var array
+   */
+  protected $fields = array();
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Mapper: Multilingual fields',
+      'description' => 'Tests Feeds multilingual support.',
+      'group' => 'Feeds',
+      'dependencies' => array('date', 'entity_translation', 'i18n_taxonomy', 'link'),
+    );
+  }
+
+  public function setUp() {
+    $modules = array(
+      'locale',
+      'entity_translation',
+      'date',
+      'link',
+      'number',
+    );
+
+    $permissions = array(
+      'administer entity translation',
+      'translate any entity',
+      'administer languages',
+    );
+
+    parent::setUp($modules, $permissions);
+
+    // Include FeedsProcessor.inc so processor related constants are available.
+    module_load_include('inc', 'feeds', 'plugins/FeedsProcessor');
+
+    // Add French language.
+    $this->addLanguage('fr', 'French');
+
+    // Add Categories vocabulary.
+    $edit = array(
+      'name' => 'Categories',
+      'machine_name' => 'categories',
+    );
+    $this->drupalPost('admin/structure/taxonomy/add', $edit, 'Save');
+
+    // Create content type.
+    $this->fields = array(
+      'date' => array(
+        'type' => 'date',
+        'settings' => array(
+          'field[settings][granularity][hour]' => FALSE,
+          'field[settings][granularity][minute]' => FALSE,
+          'field[settings][tz_handling]' => 'none',
+        ),
+      ),
+      'datestamp' => array(
+        'type' => 'datestamp',
+        'settings' => array(
+          'field[settings][granularity][second]' => TRUE,
+          'field[settings][tz_handling]' => 'utc',
+        ),
+      ),
+      'datetime' => array(
+        'type' => 'datetime',
+        'settings' => array(
+          'field[settings][granularity][second]' => TRUE,
+          'field[settings][tz_handling]' => 'utc',
+        ),
+      ),
+      'image' => array(
+        'type' => 'image',
+        'instance_settings' => array(
+          'instance[settings][alt_field]' => 1,
+          'instance[settings][title_field]' => 1,
+        ),
+      ),
+      'link' => 'link_field',
+      'number_integer' => 'number_integer',
+      'number_decimal' => 'number_decimal',
+      'number_float' => 'number_float',
+      'text' => 'text',
+    );
+    $this->contentType = $this->createContentType(array(), $this->fields);
+
+    // Create term reference field.
+    $field = array(
+      'field_name' => 'field_category',
+      'type' => 'taxonomy_term_reference',
+      'cardinality' => FIELD_CARDINALITY_UNLIMITED,
+      'settings' => array(
+        'allowed_values' => array(
+          array(
+            'vocabulary' => 'categories',
+            'parent' => 0,
+          ),
+        ),
+      ),
+    );
+    field_create_field($field);
+
+    // Add term reference field to article bundle.
+    $this->instance = array(
+      'field_name' => 'field_category',
+      'bundle' => $this->contentType,
+      'entity_type' => 'node',
+      'widget' => array(
+        'type' => 'taxonomy_autocomplete',
+      ),
+      'display' => array(
+        'default' => array(
+          'type' => 'taxonomy_term_reference_link',
+        ),
+      ),
+    );
+    field_create_instance($this->instance);
+
+    // Make content type and fields multilingual.
+    $field_names = array(
+      'body',
+      'field_category',
+    );
+    foreach ($this->fields as $field_name => $field_type) {
+      $field_names[] = 'field_' . $field_name;
+    }
+    $this->setupMultilingual($this->contentType, $field_names);
+
+    // Copy directory of source files, CSV file expects them in public://images.
+    $this->copyDir($this->absolutePath() . '/tests/feeds/assets', 'public://images');
+
+    // Create an importer configuration with basic mapping.
+    $this->createImporterConfiguration('Test multilingual fields import from CSV', 'node');
+    $this->setPlugin('node', 'FeedsCSVParser');
+    $this->setPlugin('node', 'FeedsFileFetcher');
+    $this->setSettings('node', 'FeedsNodeProcessor', array(
+      'bundle' => $this->contentType,
+      'language' => 'en',
+    ));
+
+    // Add language neutral mappings.
+    $this->addMappings('node', array(
+      0 => array(
+        'source' => 'guid',
+        'target' => 'guid',
+        'unique' => 1,
+      ),
+      1 => array(
+        'source' => 'title',
+        'target' => 'title',
+      ),
+    ));
+  }
+
+  /**
+   * Tests multilingual mappings to translatable fields (entity translation).
+   */
+  public function testMultilingualFieldMappings() {
+    // Add English mappers.
+    $index = 2;
+    $mappings = $this->getMappingsInLanguage('en', $index);
+    // Append "_en" to each source name.
+    foreach ($mappings as &$mapping) {
+      $mapping['source'] .= '_en';
+    }
+    $this->addMappings('node', $mappings);
+    $index += count($mappings);
+
+    // Add French mappers.
+    $mappings = $this->getMappingsInLanguage('fr', $index);
+    // Append "_fr" to each source name.
+    foreach ($mappings as &$mapping) {
+      $mapping['source'] .= '_fr';
+    }
+    $this->addMappings('node', $mappings);
+
+    // Import file that has items with both English and French field values.
+    $this->importFile('node', $this->absolutePath() . '/tests/feeds/multilingual_en_fr.csv');
+    $this->assertText(t('Created 1 node'));
+
+    // Load node.
+    $node = node_load(1, NULL, TRUE);
+
+    // Inspect availability of English values.
+    $english = $this->getEnglishValues($node) + array(
+      'field_category' => array(
+        'expected' => 1,
+        'actual' => $node->field_category['en'][0]['tid'],
+      ),
+    );
+    foreach ($english as $field_name => $value) {
+      $this->assertEqual($value['expected'], $value['actual'], format_string('The English field %field has the expected value (actual: @actual).', array('%field' => $field_name, '@actual' => $value['actual'])));
+    }
+
+    // Inspect availability of French values.
+    $french = $this->getFrenchValues($node) + array(
+      'field_category' => array(
+        'expected' => 2,
+        'actual' => $node->field_category['fr'][0]['tid'],
+      ),
+    );
+    foreach ($french as $field_name => $value) {
+      $this->assertEqual($value['expected'], $value['actual'], format_string('The French field %field has the expected value (actual: @actual).', array('%field' => $field_name, '@actual' => $value['actual'])));
+    }
+  }
+
+  /**
+   * Tests if values of fields in other languages are kept when not importing
+   * in that language.
+   */
+  public function testChangedLanguageImport() {
+    // Add Dutch language.
+    $this->addLanguage('nl', 'Dutch');
+
+    // Import an item first in the Dutch language.
+    $this->setSettings('node', 'FeedsNodeProcessor', array(
+      'language' => 'nl',
+    ));
+    $mappings = $this->getMappingsInLanguage('nl', 2);
+    $this->addMappings('node', $mappings);
+    $this->importFile('node', $this->absolutePath() . '/tests/feeds/multilingual_nl.csv');
+    $this->assertText(t('Created 1 node'));
+
+    // Assert that Dutch values were created.
+    $node = node_load(1, NULL, TRUE);
+    $dutch = $this->getDutchValues($node) + array(
+      'field_category' => array(
+        'expected' => 1,
+        'actual' => $node->field_category['nl'][0]['tid'],
+      ),
+    );
+    foreach ($dutch as $field_name => $value) {
+      $this->assertEqual($value['expected'], $value['actual'], format_string('The Dutch field %field has the expected value (actual: @actual).', array('%field' => $field_name, '@actual' => $value['actual'])));
+    }
+
+    // Set import to update existing nodes.
+    $this->setSettings('node', 'FeedsNodeProcessor', array(
+      'update_existing' => FEEDS_UPDATE_EXISTING,
+    ));
+
+    // Change mappers language to French.
+    $path = 'admin/structure/feeds/node/mapping';
+    foreach ($mappings as $i => $mapping) {
+      $this->drupalPostAJAX($path, array(), 'mapping_settings_edit_' . $i);
+      $edit = array("config[$i][settings][field_language]" => 'fr');
+      $this->drupalPostAJAX(NULL, $edit, 'mapping_settings_update_' . $i);
+      $this->drupalPost(NULL, array(), t('Save'));
+    }
+    // Import French item.
+    $this->importFile('node', $this->absolutePath() . '/tests/feeds/multilingual_fr.csv');
+    $this->assertText(t('Updated 1 node'));
+
+    // Assert that French values were created.
+    $node = node_load(1, NULL, TRUE);
+    $french = $this->getFrenchValues($node) + array(
+      'field_category' => array(
+        'expected' => 2,
+        'actual' => $node->field_category['fr'][0]['tid'],
+      ),
+    );
+    foreach ($french as $field_name => $value) {
+      $this->assertEqual($value['expected'], $value['actual'], format_string('The French field %field has the expected value (actual: @actual).', array('%field' => $field_name, '@actual' => $value['actual'])));
+    }
+
+    // Assert that Dutch values still exist.
+    $dutch = $this->getDutchValues($node) + array(
+      'field_category' => array(
+        'expected' => 1,
+        'actual' => $node->field_category['nl'][0]['tid'],
+      ),
+    );
+    foreach ($dutch as $field_name => $value) {
+      $this->assertEqual($value['expected'], $value['actual'], format_string('The Dutch field %field has the expected value (actual: @actual).', array('%field' => $field_name, '@actual' => $value['actual'])));
+    }
+  }
+
+  /**
+   * Tests if values of fields in other languages are kept when not importing
+   * in that language for nodes that were not created by Feeds.
+   */
+  public function testChangedLanguageImportForExistingNode() {
+    // Add Dutch language.
+    $this->addLanguage('nl', 'Dutch');
+
+    // Date settings.
+    foreach (array('datestamp', 'datetime') as $field) {
+      $field = 'field_' . $field;
+      $edit = array(
+        'field[settings][granularity][second]' => 1,
+      );
+      $this->drupalPost('admin/structure/types/manage/' . $this->contentType . '/fields/' . $field . '/field-settings', $edit, 'Save field settings');
+    }
+
+    // Hack to get date fields to not round to every 15 seconds.
+    foreach (array('date', 'datestamp', 'datetime') as $field) {
+      $field = 'field_' . $field;
+      $edit = array(
+        'widget_type' => 'date_select',
+      );
+      $this->drupalPost('admin/structure/types/manage/' . $this->contentType . '/fields/' . $field . '/widget-type', $edit, 'Continue');
+      $edit = array(
+        'instance[widget][settings][increment]' => 1,
+        'field[settings][enddate_get]' => 1,
+      );
+      $this->drupalPost('admin/structure/types/manage/' . $this->contentType . '/fields/' . $field, $edit, 'Save settings');
+      $edit = array(
+        'widget_type' => 'date_text',
+      );
+      $this->drupalPost('admin/structure/types/manage/' . $this->contentType . '/fields/' . $field . '/widget-type', $edit, 'Continue');
+    }
+
+    // Create a node with Dutch values.
+    $edit = array(
+      'title' => 'Teste Feeds Multilingue 1',
+      'body[und][0][value]' => 'Dit is de berichttekst',
+      'field_date[und][0][value][date]' => '07/29/1985',
+      'field_datestamp[und][0][value][date]' => '07/29/1985 - 04:48:12',
+      'field_datetime[und][0][value][date]' => '07/29/1985 - 04:48:12',
+      'field_link[und][0][url]' => 'http://google.nl',
+      'field_number_decimal[und][0][value]' => '30.3',
+      'field_number_float[und][0][value]' => '30.2795',
+      'field_number_integer[und][0][value]' => '30',
+      'field_text[und][0][value]' => 'Wortelen',
+      'files[field_image_und_0]' => drupal_realpath('public://images/attersee.jpeg'),
+      'field_category[und]' => 'Nieuws',
+      'language' => 'nl',
+    );
+    $this->drupalPost('node/add/' . $this->contentType, $edit, t('Save'));
+    // Add alt/title to the image.
+    $edit = array(
+      'field_image[nl][0][alt]' => 'Bij het zien',
+      'field_image[nl][0][title]' => 'Bij het zien van de groene vloeistof',
+    );
+    $this->drupalPost('node/1/edit/nl', $edit, t('Save'));
+    $this->drupalGet('node/1/edit/nl');
+
+    // Assert that the Dutch values were put in as expected.
+    $node = node_load(1, NULL, TRUE);
+    $dutch = $this->getDutchValues($node) + array(
+      'field_category' => array(
+        'expected' => 1,
+        'actual' => $node->field_category['nl'][0]['tid'],
+      ),
+    );
+    foreach ($dutch as $field_name => $value) {
+      $this->assertEqual($value['expected'], $value['actual'], format_string('The Dutch field %field has the expected value (actual: @actual).', array('%field' => $field_name, '@actual' => $value['actual'])));
+    }
+
+    // Change unique target from guid (0) to title (1).
+    $path = 'admin/structure/feeds/node/mapping';
+    $this->drupalPostAJAX($path, array(), 'mapping_settings_edit_0');
+    $edit = array("config[0][settings][unique]" => FALSE);
+    $this->drupalPostAJAX(NULL, $edit, 'mapping_settings_update_0');
+    $this->drupalPost(NULL, array(), t('Save'));
+    $this->drupalPostAJAX($path, array(), 'mapping_settings_edit_1');
+    $edit = array("config[1][settings][unique]" => 1);
+    $this->drupalPostAJAX(NULL, $edit, 'mapping_settings_update_1');
+    $this->drupalPost(NULL, array(), t('Save'));
+
+    // Update this item with Feeds.
+    $this->setSettings('node', 'FeedsNodeProcessor', array(
+      'update_existing' => FEEDS_UPDATE_EXISTING,
+    ));
+    $this->addMappings('node', $this->getMappingsInLanguage('fr'));
+    $this->importFile('node', $this->absolutePath() . '/tests/feeds/multilingual_fr.csv');
+    $this->assertText(t('Updated 1 node'));
+
+    // Assert that French values were created.
+    $node = node_load(1, NULL, TRUE);
+    $french = $this->getFrenchValues($node) + array(
+      'field_category' => array(
+        'expected' => 2,
+        'actual' => $node->field_category['fr'][0]['tid'],
+      ),
+    );
+    foreach ($french as $field_name => $value) {
+      $this->assertEqual($value['expected'], $value['actual'], format_string('The French field %field has the expected value (actual: @actual).', array('%field' => $field_name, '@actual' => $value['actual'])));
+    }
+
+    // Assert that Dutch values still exist.
+    $dutch = $this->getDutchValues($node) + array(
+      'field_category' => array(
+        'expected' => 1,
+        'actual' => $node->field_category['nl'][0]['tid'],
+      ),
+    );
+    foreach ($dutch as $field_name => $value) {
+      $this->assertEqual($value['expected'], $value['actual'], format_string('The Dutch field %field has the expected value (actual: @actual).', array('%field' => $field_name, '@actual' => $value['actual'])));
+    }
+  }
+
+  /**
+   * Tests if fields still are imported in their language when the
+   * entity_translation module gets disabled.
+   *
+   * The entity_translation module is mainly an UI module for configuring field
+   * language and disabling that module should not have effect on importing
+   * values in a specific language for fields.
+   */
+  public function testWithDisabledEntityTranslationModule() {
+    module_disable(array('entity_translation'));
+    // Make sure that entity info is reset.
+    drupal_flush_all_caches();
+    drupal_static_reset();
+
+    // Configure importer to import in French language.
+    $this->setSettings('node', 'FeedsNodeProcessor', array(
+      'language' => 'fr',
+    ));
+    $this->addMappings('node', $this->getMappingsInLanguage('fr'));
+
+    // Import content.
+    $this->importFile('node', $this->absolutePath() . '/tests/feeds/multilingual_fr.csv');
+    $this->assertText(t('Created 1 node'));
+
+    // Assert that the fields were all created in French.
+    $node = node_load(1, NULL, TRUE);
+    $french = $this->getFrenchValues($node) + array(
+      'field_category' => array(
+        'expected' => 1,
+        'actual' => $node->field_category['fr'][0]['tid'],
+      ),
+    );
+    foreach ($french as $field_name => $value) {
+      $this->assertEqual($value['expected'], $value['actual'], format_string('The field %field has the expected value (actual: @actual).', array('%field' => $field_name, '@actual' => $value['actual'])));
+    }
+  }
+
+  /**
+   * Tests if fields are still imported in their language when the
+   * entity_translation module gets uninstalled.
+   *
+   * @see testWithDisabledEntityTranslationModule()
+   */
+  public function testWithUninstalledEntityTranslationModule() {
+    module_disable(array('entity_translation'));
+    drupal_uninstall_modules(array('entity_translation'));
+    // Make sure that entity info is reset.
+    drupal_flush_all_caches();
+    drupal_static_reset();
+
+    // Configure importer to import in French language.
+    $this->setSettings('node', 'FeedsNodeProcessor', array(
+      'language' => 'fr',
+    ));
+    $this->addMappings('node', $this->getMappingsInLanguage('fr'));
+
+    // Import content.
+    $this->importFile('node', $this->absolutePath() . '/tests/feeds/multilingual_fr.csv');
+    $this->assertText(t('Created 1 node'));
+
+    // Assert that the fields were all created in French.
+    $node = node_load(1, NULL, TRUE);
+    $french = $this->getFrenchValues($node) + array(
+      'field_category' => array(
+        'expected' => 1,
+        'actual' => $node->field_category['fr'][0]['tid'],
+      ),
+    );
+    foreach ($french as $field_name => $value) {
+      $this->assertEqual($value['expected'], $value['actual'], format_string('The field %field has the expected value (actual: @actual).', array('%field' => $field_name, '@actual' => $value['actual'])));
+    }
+  }
+
+  /**
+   * Tests if fields are imported in LANGUAGE_NONE if the field's language gets
+   * disabled after configuring.
+   */
+  public function testDisabledLanguage() {
+    // Configure importer to import in French language.
+    $this->setSettings('node', 'FeedsNodeProcessor', array(
+      'language' => 'fr',
+    ));
+    $this->addMappings('node', $this->getMappingsInLanguage('fr'));
+
+    // Now disable the French language.
+    $path = 'admin/config/regional/language';
+    $edit = array(
+      'enabled[fr]' => FALSE,
+    );
+    $this->drupalPost($path, $edit, t('Save configuration'));
+    // Reset static cache to update the available languages.
+    drupal_static_reset();
+
+    // Ensure no error messages are shown on the mappings page.
+    $this->drupalGet('admin/structure/feeds/node/mapping');
+
+    // Import content.
+    $this->importFile('node', $this->absolutePath() . '/tests/feeds/multilingual_fr.csv');
+    $this->assertText(t('Created 1 node'));
+
+    // Assert that the fields were all created in LANGUAGE_NONE.
+    $node = node_load(1, NULL, TRUE);
+    $french = $this->getFrenchValues($node, LANGUAGE_NONE) + array(
+      'field_category' => array(
+        'expected' => 1,
+        'actual' => $node->field_category[LANGUAGE_NONE][0]['tid'],
+      ),
+    );
+    foreach ($french as $field_name => $value) {
+      $this->assertEqual($value['expected'], $value['actual'], format_string('The field %field has the expected value (actual: @actual).', array('%field' => $field_name, '@actual' => $value['actual'])));
+    }
+  }
+
+  /**
+   * Tests if fields are imported in LANGUAGE_NONE if the field's language gets
+   * removed after configuring.
+   */
+  public function testRemovedLanguage() {
+    // Configure importer to import in French language.
+    $this->setSettings('node', 'FeedsNodeProcessor', array(
+      'language' => 'fr',
+    ));
+    $this->addMappings('node', $this->getMappingsInLanguage('fr'));
+
+    // Now remove the French language.
+    $path = 'admin/config/regional/language/delete/fr';
+    $this->drupalPost($path, array(), t('Delete'));
+    // Reset static cache to update the available languages.
+    drupal_static_reset();
+
+    // Import content.
+    $this->importFile('node', $this->absolutePath() . '/tests/feeds/multilingual_fr.csv');
+    $this->assertText(t('Created 1 node'));
+
+    // Assert that the fields were all created in LANGUAGE_NONE.
+    $node = node_load(1, NULL, TRUE);
+    $french = $this->getFrenchValues($node, LANGUAGE_NONE) + array(
+      'field_category' => array(
+        'expected' => 1,
+        'actual' => $node->field_category[LANGUAGE_NONE][0]['tid'],
+      ),
+    );
+    foreach ($french as $field_name => $value) {
+      $this->assertEqual($value['expected'], $value['actual'], format_string('The field %field has the expected value (actual: @actual).', array('%field' => $field_name, '@actual' => $value['actual'])));
+    }
+  }
+
+  /**
+   * Tests if autocreated terms are in the language that was set on the target configuration
+   * in case the taxonomy is multilingual.
+   */
+  public function testAutocreatedTermLanguage() {
+    module_enable(array('i18n_taxonomy'));
+    // Make sure that entity info is reset.
+    drupal_flush_all_caches();
+    drupal_static_reset();
+
+    // Enable multilingual taxonomy.
+    $edit = array('i18n_mode' => 4);
+    $this->drupalPost('admin/structure/taxonomy/categories/edit', $edit, 'Save');
+
+    // Configure importer to import in French language.
+    $this->setSettings('node', 'FeedsNodeProcessor', array(
+      'language' => 'fr',
+    ));
+    $this->addMappings('node', array(
+      2 => array(
+        'source' => 'term',
+        'target' => 'field_category',
+        'autocreate' => TRUE,
+        'field_language' => 'fr',
+      ),
+    ));
+
+    // Import French item.
+    $this->importFile('node', $this->absolutePath() . '/tests/feeds/multilingual_fr.csv');
+    $this->assertText(t('Created 1 node'));
+
+    // Assert that the created term is in the French language.
+    $term = taxonomy_term_load(1);
+    $this->assertEqual('fr', entity_language('taxonomy_term', $term));
+  }
+
+  /**
+   * Tests if values are cleared out when an empty value or no value is
+   * provided.
+   */
+  public function testClearOutValues() {
+    // Set to update existing nodes.
+    $this->setSettings('node', 'FeedsNodeProcessor', array(
+      'update_existing' => FEEDS_UPDATE_EXISTING,
+    ));
+
+    // Add English mappers.
+    $index = 2;
+    $mappings = $this->getMappingsInLanguage('en', $index);
+    // Append "_en" to each source name.
+    foreach ($mappings as &$mapping) {
+      $mapping['source'] .= '_en';
+    }
+    $this->addMappings('node', $mappings);
+    $index += count($mappings);
+
+    // Add French mappers.
+    $mappings = $this->getMappingsInLanguage('fr', $index);
+    // Append "_fr" to each source name.
+    foreach ($mappings as &$mapping) {
+      $mapping['source'] .= '_fr';
+    }
+    $this->addMappings('node', $mappings);
+
+    // Import file that has items with both English and French field values.
+    $this->importFile('node', $this->absolutePath() . '/tests/feeds/multilingual_en_fr.csv');
+    $this->assertText(t('Created 1 node'));
+
+    // Now import a file where the French remained, but the English values were
+    // removed.
+    $this->importFile('node', $this->absolutePath() . '/tests/feeds/multilingual_en_fr_empty.csv');
+    $this->assertText(t('Updated 1 node'));
+
+    // Load node.
+    $node = node_load(1, NULL, TRUE);
+
+    // Check that the English values are gone, but the French values are still
+    // there.
+    $fields = array(
+      'body',
+      'field_date',
+      'field_datestamp',
+      'field_datetime',
+      'field_image',
+      'field_link',
+      'field_number_decimal',
+      'field_number_float',
+      'field_number_integer',
+      'field_category',
+      'field_text',
+    );
+    foreach ($fields as $field_name) {
+      $this->assertTrue(empty($node->{$field_name}['en']), format_string('The field %field is empty.', array('%field' => $field_name)));
+    }
+
+    // Inspect availability of French values.
+    $french = $this->getFrenchValues($node) + array(
+      'field_category' => array(
+        'expected' => 2,
+        'actual' => $node->field_category['fr'][0]['tid'],
+      ),
+    );
+    // Since the image was placed on the node again, its file name is now
+    // "la fayette_0.jpeg."
+    $french['field_image']['expected'] = 'la fayette_0.jpeg';
+    foreach ($french as $field_name => $value) {
+      $this->assertEqual($value['expected'], $value['actual'], format_string('The French field %field has the expected value (actual: @actual).', array('%field' => $field_name, '@actual' => $value['actual'])));
+    }
+  }
+
+  /**
+   * Tests if values are cleared out when an empty value is provided for a
+   * language that got disabled.
+   */
+  public function testClearOutValuesWithDisabledLanguage() {
+    // Set to update existing nodes.
+    $this->setSettings('node', 'FeedsNodeProcessor', array(
+      'update_existing' => FEEDS_UPDATE_EXISTING,
+    ));
+
+    // Configure importer to import in French language.
+    $this->setSettings('node', 'FeedsNodeProcessor', array(
+      'language' => 'fr',
+    ));
+    $this->addMappings('node', $this->getMappingsInLanguage('fr'));
+
+    // Now disable the French language.
+    $path = 'admin/config/regional/language';
+    $edit = array(
+      'enabled[fr]' => FALSE,
+    );
+    $this->drupalPost($path, $edit, t('Save configuration'));
+
+    // Ensure no error messages are shown on the mappings page.
+    $this->drupalGet('admin/structure/feeds/node/mapping');
+
+    // Import content. Since the French language was disabled, the content
+    // should be imported as LANGUAGE_NONE.
+    // @see ::testDisabledLanguage()
+    $this->importFile('node', $this->absolutePath() . '/tests/feeds/multilingual_fr.csv');
+    $this->assertText(t('Created 1 node'));
+
+    // Now import a file with empty values.
+    $this->importFile('node', $this->absolutePath() . '/tests/feeds/multilingual_empty.csv');
+    $this->assertText(t('Updated 1 node'));
+
+    // Load node.
+    $node = node_load(1, NULL, TRUE);
+
+    // Check that the values in LANGUAGE_NONE are gone.
+    $fields = array(
+      'body',
+      'field_date',
+      'field_datestamp',
+      'field_datetime',
+      'field_image',
+      'field_link',
+      'field_number_decimal',
+      'field_number_float',
+      'field_number_integer',
+      'field_category',
+      'field_text',
+    );
+    foreach ($fields as $field_name) {
+      $this->assertTrue(empty($node->{$field_name}[LANGUAGE_NONE]), format_string('The field %field is empty.', array('%field' => $field_name)));
+    }
+  }
+
+  /**
+   * Adds a language to test with.
+   *
+   * @param string $langcode
+   *   The language's langcode.
+   * @param string $label
+   *   The language human readable name.
+   */
+  protected function addLanguage($langcode, $label) {
+    $edit = array(
+      'langcode' => $langcode,
+    );
+    $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
+    $this->assertText(format_string('The language @language has been created and can now be used.', array('@language' => $label)));
+  }
+
+  /**
+   * Sets given content type and fields to be translatable.
+   *
+   * @param string $typename
+   *   The machine name of the node type.
+   * @param array $field_names
+   *   The fields to enable multilingual support for.
+   */
+  protected function setupMultilingual($typename, array $field_names) {
+    // Enable entity field translation for content type.
+    $edit = array(
+      'language_content_type' => 4,
+      'entity_translation_hide_translation_links' => 1,
+      'entity_translation_node_metadata' => 0,
+    );
+    $this->drupalPost('admin/structure/types/manage/' . $typename, $edit, t('Save content type'));
+
+    // Enable field translation on fields.
+    $edit = array(
+      'field[translatable]' => 1,
+    );
+    foreach ($field_names as $field_name) {
+      $this->drupalPost("admin/structure/types/manage/{$typename}/fields/{$field_name}", $edit, t('Save settings'));
+    }
+
+    // Reset static cache so that all languages are available when
+    // field_available_languages() is called during node_load().
+    drupal_static_reset();
+  }
+
+  /**
+   * Adds mappings for each field in specified language.
+   *
+   * @param string $langcode
+   *   The code of the desired language.
+   * @param int $start
+   *   The index number to start the array with. This must be
+   *   specified in order to add mappings to the right index when
+   *   calling FeedsWebTestCase::addMappings().
+   */
+  protected function getMappingsInLanguage($langcode, $start = 2) {
+    $mappings = array(
+      $start => array(
+        'source' => 'body',
+        'target' => 'body',
+      ),
+      array(
+        'source' => 'date',
+        'target' => 'field_date:start',
+      ),
+      array(
+        'source' => 'datestamp',
+        'target' => 'field_datestamp:start',
+      ),
+      array(
+        'source' => 'datetime',
+        'target' => 'field_datetime:start',
+      ),
+      array(
+        'source' => 'image',
+        'target' => 'field_image:uri',
+      ),
+      array(
+        'source' => 'image_alt',
+        'target' => 'field_image:alt',
+      ),
+      array(
+        'source' => 'image_title',
+        'target' => 'field_image:title',
+      ),
+      array(
+        'source' => 'link',
+        'target' => 'field_link:url',
+      ),
+      array(
+        'source' => 'number_decimal',
+        'target' => 'field_number_decimal',
+      ),
+      array(
+        'source' => 'number_float',
+        'target' => 'field_number_float',
+      ),
+      array(
+        'source' => 'number_integer',
+        'target' => 'field_number_integer',
+      ),
+      array(
+        'source' => 'term',
+        'target' => 'field_category',
+        'autocreate' => TRUE,
+      ),
+      array(
+        'source' => 'text',
+        'target' => 'field_text',
+      ),
+    );
+    foreach ($mappings as &$mapping) {
+      $mapping['field_language'] = $langcode;
+    }
+    return $mappings;
+  }
+
+  /**
+   * Returns expected and actual values of given node for the Dutch language.
+   *
+   * @param object $node
+   *   The multilingual node.
+   * @param string $langcode
+   *   The used language code.
+   *
+   * @return array
+   *   The expected and actual Dutch values.
+   */
+  protected function getDutchValues($node, $langcode = 'nl') {
+    return array(
+      'body' => array(
+        'expected' => 'Dit is de berichttekst',
+        'actual' => $node->body[$langcode][0]['value'],
+      ),
+      'field_date' => array(
+        'expected' => '1985-07-29T00:00:00',
+        'actual' => $node->field_date[$langcode][0]['value'],
+      ),
+      'field_datestamp' => array(
+        'expected' => '491460492',
+        'actual' => $node->field_datestamp[$langcode][0]['value'],
+      ),
+      'field_datetime' => array(
+        'expected' => '1985-07-29 04:48:12',
+        'actual' => $node->field_datetime[$langcode][0]['value'],
+      ),
+      'field_image' => array(
+        'expected' => 'attersee.jpeg',
+        'actual' => $node->field_image[$langcode][0]['filename'],
+      ),
+      'field_image:alt' => array(
+        'expected' => 'Bij het zien',
+        'actual' => $node->field_image[$langcode][0]['alt'],
+      ),
+      'field_image:title' => array(
+        'expected' => 'Bij het zien van de groene vloeistof',
+        'actual' => $node->field_image[$langcode][0]['title'],
+      ),
+      'field_link' => array(
+        'expected' => 'http://google.nl',
+        'actual' => $node->field_link[$langcode][0]['url'],
+      ),
+      'field_number_decimal' => array(
+        'expected' => 30.3,
+        'actual' => $node->field_number_decimal[$langcode][0]['value'],
+      ),
+      'field_number_float' => array(
+        'expected' => 30.2795,
+        'actual' => $node->field_number_float[$langcode][0]['value'],
+      ),
+      'field_number_integer' => array(
+        'expected' => 30,
+        'actual' => $node->field_number_integer[$langcode][0]['value'],
+      ),
+      'field_text' => array(
+        'expected' => 'Wortelen',
+        'actual' => $node->field_text[$langcode][0]['value'],
+      ),
+    );
+  }
+
+  /**
+   * Returns expected and actual values of given node for the English language.
+   *
+   * @param object $node
+   *   The multilingual node.
+   * @param string $langcode
+   *   The used language code.
+   *
+   * @return array
+   *   The expected and actual English values.
+   */
+  protected function getEnglishValues($node, $langcode = 'en') {
+    return array(
+      'body' => array(
+        'expected' => 'This is the body',
+        'actual' => $node->body[$langcode][0]['value'],
+      ),
+      'field_date' => array(
+        'expected' => '2015-10-21T00:00:00',
+        'actual' => $node->field_date[$langcode][0]['value'],
+      ),
+      'field_datestamp' => array(
+        'expected' => '1445470140',
+        'actual' => $node->field_datestamp[$langcode][0]['value'],
+      ),
+      'field_datetime' => array(
+        'expected' => '2015-10-21 23:29:00',
+        'actual' => $node->field_datetime[$langcode][0]['value'],
+      ),
+      'field_image' => array(
+        'expected' => 'foosball.jpeg',
+        'actual' => $node->field_image[$langcode][0]['filename'],
+      ),
+      'field_image:alt' => array(
+        'expected' => 'Foosball',
+        'actual' => $node->field_image[$langcode][0]['alt'],
+      ),
+      'field_image:title' => array(
+        'expected' => 'Foosball played by two guys',
+        'actual' => $node->field_image[$langcode][0]['title'],
+      ),
+      'field_link' => array(
+        'expected' => 'http://google.ca',
+        'actual' => $node->field_link[$langcode][0]['url'],
+      ),
+      'field_number_decimal' => array(
+        'expected' => 4.2,
+        'actual' => $node->field_number_decimal[$langcode][0]['value'],
+      ),
+      'field_number_float' => array(
+        'expected' => 3.1416,
+        'actual' => $node->field_number_float[$langcode][0]['value'],
+      ),
+      'field_number_integer' => array(
+        'expected' => 1000,
+        'actual' => $node->field_number_integer[$langcode][0]['value'],
+      ),
+      'field_text' => array(
+        'expected' => 'Carrots',
+        'actual' => $node->field_text[$langcode][0]['value'],
+      ),
+    );
+  }
+
+  /**
+   * Returns expected and actual values of given node for the French language.
+   *
+   * @param object $node
+   *   The multilingual node.
+   * @param string $langcode
+   *   The used language code.
+   *
+   * @return array
+   *   The expected and actual French values.
+   */
+  protected function getFrenchValues($node, $langcode = 'fr') {
+    return array(
+      'body' => array(
+        'expected' => 'Ceci est la corps',
+        'actual' => $node->body[$langcode][0]['value'],
+      ),
+      'field_date' => array(
+        'expected' => '1955-11-05T00:00:00',
+        'actual' => $node->field_date[$langcode][0]['value'],
+      ),
+      'field_datestamp' => array(
+        'expected' => '-446731187',
+        'actual' => $node->field_datestamp[$langcode][0]['value'],
+      ),
+      'field_datetime' => array(
+        'expected' => '1955-11-05 12:00:13',
+        'actual' => $node->field_datetime[$langcode][0]['value'],
+      ),
+      'field_image' => array(
+        'expected' => 'la fayette.jpeg',
+        'actual' => $node->field_image[$langcode][0]['filename'],
+      ),
+      'field_image:alt' => array(
+        'expected' => 'La Fayette',
+        'actual' => $node->field_image[$langcode][0]['alt'],
+      ),
+      'field_image:title' => array(
+        'expected' => 'la Fayette dans les bois',
+        'actual' => $node->field_image[$langcode][0]['title'],
+      ),
+      'field_link' => array(
+        'expected' => 'http://google.fr',
+        'actual' => $node->field_link[$langcode][0]['url'],
+      ),
+      'field_number_decimal' => array(
+        'expected' => 1.2,
+        'actual' => $node->field_number_decimal[$langcode][0]['value'],
+      ),
+      'field_number_float' => array(
+        'expected' => 5.6295,
+        'actual' => $node->field_number_float[$langcode][0]['value'],
+      ),
+      'field_number_integer' => array(
+        'expected' => 2000,
+        'actual' => $node->field_number_integer[$langcode][0]['value'],
+      ),
+      'field_text' => array(
+        'expected' => 'Carottes',
+        'actual' => $node->field_text[$langcode][0]['value'],
+      ),
+    );
+  }
+}
diff --git a/tests/feeds_mapper_taxonomy.test b/tests/feeds_mapper_taxonomy.test
index ed55751..de83191 100644
--- a/tests/feeds_mapper_taxonomy.test
+++ b/tests/feeds_mapper_taxonomy.test
@@ -234,6 +234,7 @@ class FeedsMapperTaxonomyTestCase extends FeedsMapperTestCase {
     $target = 'field_tags';
     $mapping = array(
       'term_search' => FEEDS_TAXONOMY_SEARCH_TERM_ID,
+      'language' => LANGUAGE_NONE,
     );
 
     $source = FeedsSource::instance('tmp', 0);
@@ -284,6 +285,7 @@ class FeedsMapperTaxonomyTestCase extends FeedsMapperTestCase {
     $target = 'field_tags';
     $mapping = array(
       'term_search' => FEEDS_TAXONOMY_SEARCH_TERM_GUID,
+      'language' => LANGUAGE_NONE,
     );
 
     $source = FeedsSource::instance('tmp', 0);
diff --git a/tests/feeds_tests.module b/tests/feeds_tests.module
index fa89462..2eea4d7 100644
--- a/tests/feeds_tests.module
+++ b/tests/feeds_tests.module
@@ -209,7 +209,7 @@ function feeds_tests_feeds_processor_targets_alter(array &$targets, $entity_type
  *
  * @see feeds_tests_feeds_processor_targets()
  */
-function feeds_tests_preprocess_callback(FeedsSource $source, $target_item, $target, array &$mapping) {
+function feeds_tests_preprocess_callback(array $target, array &$mapping) {
   $mapping['required_value'] = TRUE;
 }
 
@@ -360,7 +360,7 @@ class FeedsTestsPreprocess {
  *
  * @see feeds_tests_feeds_processor_targets()
  */
-  public static function callback(FeedsSource $source, $target_item, $target, array &$mapping) {
+  public static function callback(FeedsSource $source, $target_item, array $target, array &$mapping) {
     $mapping['required_value'] = TRUE;
   }
 
