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 @@ +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 @@ +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: Error: @error', 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 @@ + '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..99b6083 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(array $target, array &$mapping) { $mapping['required_value'] = TRUE; }