Index: modules/simpletest/drupal_web_test_case.php =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/drupal_web_test_case.php,v retrieving revision 1.132 diff -u -r1.132 drupal_web_test_case.php --- modules/simpletest/drupal_web_test_case.php 30 Jul 2009 10:46:53 -0000 1.132 +++ modules/simpletest/drupal_web_test_case.php 3 Aug 2009 18:18:25 -0000 @@ -686,7 +686,7 @@ protected function drupalCreateNode($settings = array()) { // Populate defaults array. $settings += array( - 'body' => array(array()), + 'body' => array(FIELD_LANGUAGE_NONE => array(array())), 'title' => $this->randomName(8), 'comment' => 2, 'changed' => REQUEST_TIME, @@ -723,7 +723,7 @@ 'value' => $this->randomName(32), 'format' => FILTER_FORMAT_DEFAULT ); - $settings['body'][0] += $body; + $settings['body'][FIELD_LANGUAGE_NONE][0] += $body; $node = (object) $settings; node_save($node); Index: modules/locale/locale.test =================================================================== RCS file: /cvs/drupal/drupal/modules/locale/locale.test,v retrieving revision 1.33 diff -u -r1.33 locale.test --- modules/locale/locale.test 3 Aug 2009 03:04:33 -0000 1.33 +++ modules/locale/locale.test 3 Aug 2009 18:18:21 -0000 @@ -1394,7 +1394,7 @@ $edit = array( 'type' => 'page', 'title' => $node_title, - 'body' => array(array('value' => $node_body)), + 'body' => array(FIELD_LANGUAGE_NONE => array(array('value' => $node_body))), 'language' => $langcode, ); $node = $this->drupalCreateNode($edit); Index: modules/field/field.attach.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.attach.inc,v retrieving revision 1.33 diff -u -r1.33 field.attach.inc --- modules/field/field.attach.inc 16 Jul 2009 10:30:12 -0000 1.33 +++ modules/field/field.attach.inc 3 Aug 2009 18:18:15 -0000 @@ -166,6 +166,7 @@ // Merge default options. $default_options = array( 'default' => FALSE, + 'language' => NULL ); $options += $default_options; @@ -178,32 +179,38 @@ // When in 'single field' mode, only act on the specified field. if (empty($options['field_name']) || $options['field_name'] == $field_name) { $field = field_info_field($field_name); + $field_translations = array(); + $suggested_languages = empty($options['language']) ? NULL : array($options['language']); - // Extract the field values into a separate variable, easily accessed by - // hook implementations. - $items = isset($object->$field_name) ? $object->$field_name : array(); + // Initialize field translations according to the available languages. + foreach (field_multilingual_available_languages($obj_type, $field, $suggested_languages) as $language) { + $field_translations[$language] = isset($object->{$field_name}[$language]) ? $object->{$field_name}[$language] : array(); + } // Invoke the field hook and collect results. $function = $options['default'] ? 'field_default_' . $op : $field['module'] . '_field_' . $op; if (drupal_function_exists($function)) { - $result = $function($obj_type, $object, $field, $instance, $items, $a, $b); - if (isset($result)) { - // For hooks with array results, we merge results together. - // For hooks with scalar results, we collect results in an array. - if (is_array($result)) { - $return = array_merge($return, $result); + // Iterate over all the field translations. + foreach ($field_translations as $language => $items) { + $result = $function($obj_type, $object, $field, $instance, $language, $items, $a, $b); + if (isset($result)) { + // For hooks with array results, we merge results together. + // For hooks with scalar results, we collect results in an array. + if (is_array($result)) { + $return = array_merge($return, $result); + } + else { + $return[] = $result; + } } - else { - $return[] = $result; + + // Populate $items back in the field values, but avoid replacing missing + // fields with an empty array (those are not equivalent on update). + if ($items !== array() || isset($object->{$field_name}[$language])) { + $object->{$field_name}[$language] = $items; } } } - - // Populate field values back in the object, but avoid replacing missing - // fields with an empty array (those are not equivalent on update). - if ($items !== array() || property_exists($object, $field_name)) { - $object->$field_name = $items; - } } } @@ -248,6 +255,7 @@ // Merge default options. $default_options = array( 'default' => FALSE, + 'language' => NULL ); $options += $default_options; @@ -271,10 +279,11 @@ } // Group the corresponding instances and objects. $grouped_instances[$field_name][$id] = $instance; - $grouped_objects[$field_name][$id] = $objects[$id]; - // Extract the field values into a separate variable, easily accessed - // by hook implementations. - $grouped_items[$field_name][$id] = isset($object->$field_name) ? $object->$field_name : array(); + $grouped_objects[$field_name][$id] = &$objects[$id]; + $suggested_languages = empty($options['language']) ? NULL : array($options['language']); + foreach (field_multilingual_available_languages($obj_type, $fields[$field_name], $suggested_languages) as $language) { + $grouped_items[$field_name][$language][$id] = isset($object->{$field_name}[$language]) ? $object->{$field_name}[$language] : array(); + } } } // Initialize the return value for each object. @@ -285,17 +294,20 @@ foreach ($fields as $field_name => $field) { $function = $options['default'] ? 'field_default_' . $op : $field['module'] . '_field_' . $op; if (drupal_function_exists($function)) { - $results = $function($obj_type, $grouped_objects[$field_name], $field, $grouped_instances[$field_name], $grouped_items[$field_name], $a, $b); - if (isset($results)) { - // Collect results by object. - // For hooks with array results, we merge results together. - // For hooks with scalar results, we collect results in an array. - foreach ($results as $id => $result) { - if (is_array($result)) { - $return[$id] = array_merge($return[$id], $result); - } - else { - $return[$id][] = $result; + // Iterate over all the field translations. + foreach ($grouped_items[$field_name] as $language => $items) { + $results = $function($obj_type, $grouped_objects[$field_name], $field, $grouped_instances[$field_name], $language, $grouped_items[$field_name][$language], $a, $b); + if (isset($results)) { + // Collect results by object. + foreach ($results as $id => $result) { + // For hooks with array results, we merge results together. + // For hooks with scalar results, we collect results in an array. + if (is_array($result)) { + $return[$id] = array_merge($return[$id], $result); + } + else { + $return[$id][] = $result; + } } } } @@ -304,8 +316,10 @@ // Populate field values back in the objects, but avoid replacing missing // fields with an empty array (those are not equivalent on update). foreach ($grouped_objects[$field_name] as $id => $object) { - if ($grouped_items[$field_name][$id] !== array() || property_exists($object, $field_name)) { - $object->$field_name = $grouped_items[$field_name][$id]; + foreach ($grouped_items[$field_name] as $language => $items) { + if ($grouped_items[$field_name][$language][$id] !== array() || isset($object->{$field_name}[$language])) { + $object->{$field_name}[$language] = $grouped_items[$field_name][$language][$id]; + } } } } @@ -353,12 +367,17 @@ * The form structure to fill in. * @param $form_state * An associative array containing the current state of the form. + * @param $language + * TODO: The are two ways to enter field values: predefined language and + * user provided language [...] * * TODO : document the resulting $form structure, like we do for * field_attach_view(). */ -function field_attach_form($obj_type, $object, &$form, &$form_state) { - $form += (array) _field_invoke_default('form', $obj_type, $object, $form, $form_state); +function field_attach_form($obj_type, $object, &$form, &$form_state, $language = NULL) { + // If no language is provided use the default site language. + $options = array('language' => field_multilingual_valid_language($language)); + $form += (array) _field_invoke_default('form', $obj_type, $object, $form, $form_state, $options); // Let other modules make changes to the form. foreach (module_implements('field_attach_form') as $module) { @@ -880,17 +899,46 @@ * @return * A structured content array tree for drupal_render(). */ -function field_attach_view($obj_type, $object, $build_mode = 'full') { +function field_attach_view($obj_type, $object, $build_mode = 'full', $language = NULL) { + // If no language is provided use the current UI language. + $options = array('language' => field_multilingual_valid_language($language, FALSE)); + // Let field modules sanitize their data for output. - _field_invoke('sanitize', $obj_type, $object); + _field_invoke('sanitize', $obj_type, $object, $null, $null, $options); - $output = _field_invoke_default('view', $obj_type, $object, $build_mode); + $output = _field_invoke_default('view', $obj_type, $object, $build_mode, $null, $options); // Let other modules make changes after rendering the view. drupal_alter('field_attach_view', $output, $obj_type, $object, $build_mode); return $output; +} +/** + * Populate the template variables with the field values available for rendering. + * + * The $variables array will be populated with all the field instance values associated + * with the given entity type, keyed by field name; in case of translatable fields the + * language currently chosen for display will be selected. + * + * @param $obj_type + * The type of $object; e.g. 'node' or 'user'. + * @param $object + * The object with fields to render. + * @param $element + * The structured array containing the values ready for rendering. + * @param $variables + * The variables array is passed by reference and will be populated with field values. + */ +function field_attach_preprocess($obj_type, $object, $element, &$variables) { + list(, , $bundle) = field_attach_extract_ids($obj_type, $object); + foreach (field_info_instances($bundle) as $instance) { + $field_name = $instance['field_name']; + if (isset($element[$field_name]['#language'])) { + $language = $element[$field_name]['#language']; + $variables[$field_name] = isset($object->{$field_name}[$language]) ? $object->{$field_name}[$language] : NULL; + } + } } /** Index: modules/field/field.crud.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.crud.inc,v retrieving revision 1.22 diff -u -r1.22 field.crud.inc --- modules/field/field.crud.inc 2 Aug 2009 11:24:21 -0000 1.22 +++ modules/field/field.crud.inc 3 Aug 2009 18:18:15 -0000 @@ -46,6 +46,8 @@ * - cardinality (integer) * The number of values the field can hold. Legal values are any * positive integer or FIELD_CARDINALITY_UNLIMITED. + * - translatable (integer) + * Whether the field is translatable * - locked (integer) * TODO: undefined. * - module (string, read-only) @@ -230,6 +232,7 @@ $field += array( 'cardinality' => 1, + 'translatable' => FALSE, 'locked' => FALSE, 'settings' => array(), ); Index: modules/field/field.test =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.test,v retrieving revision 1.36 diff -u -r1.36 field.test --- modules/field/field.test 2 Aug 2009 11:24:21 -0000 1.36 +++ modules/field/field.test 3 Aug 2009 18:18:18 -0000 @@ -59,6 +59,7 @@ // field_test_field_load() in field_test.module). $this->instance['settings']['test_hook_field_load'] = TRUE; field_update_instance($this->instance); + $language = FIELD_LANGUAGE_NONE; $entity_type = 'test_entity'; $values = array(); @@ -73,12 +74,12 @@ $current_revision = $revision_id; // If this is the first revision do an insert. if (!$revision_id) { - $revision[$revision_id]->{$this->field_name} = $values[$revision_id]; + $revision[$revision_id]->{$this->field_name}[$language] = $values[$revision_id]; field_attach_insert($entity_type, $revision[$revision_id]); } else { // Otherwise do an update. - $revision[$revision_id]->{$this->field_name} = $values[$revision_id]; + $revision[$revision_id]->{$this->field_name}[$language] = $values[$revision_id]; field_attach_update($entity_type, $revision[$revision_id]); } } @@ -87,12 +88,12 @@ $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); field_attach_load($entity_type, array(0 => $entity)); // Number of values per field loaded equals the field cardinality. - $this->assertEqual(count($entity->{$this->field_name}), $this->field['cardinality'], t('Currrent revision: expected number of values')); + $this->assertEqual(count($entity->{$this->field_name}[$language]), $this->field['cardinality'], t('Currrent revision: expected number of values')); for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { // The field value loaded matches the one inserted or updated. - $this->assertEqual($entity->{$this->field_name}[$delta]['value'] , $values[$current_revision][$delta]['value'], t('Currrent revision: expected value %delta was found.', array('%delta' => $delta))); + $this->assertEqual($entity->{$this->field_name}[$language][$delta]['value'] , $values[$current_revision][$delta]['value'], t('Currrent revision: expected value %delta was found.', array('%delta' => $delta))); // The value added in hook_field_load() is found. - $this->assertEqual($entity->{$this->field_name}[$delta]['additional_key'], 'additional_value', t('Currrent revision: extra information for value %delta was found', array('%delta' => $delta))); + $this->assertEqual($entity->{$this->field_name}[$language][$delta]['additional_key'], 'additional_value', t('Currrent revision: extra information for value %delta was found', array('%delta' => $delta))); } // Confirm each revision loads the correct data. @@ -100,12 +101,12 @@ $entity = field_test_create_stub_entity(0, $revision_id, $this->instance['bundle']); field_attach_load_revision($entity_type, array(0 => $entity)); // Number of values per field loaded equals the field cardinality. - $this->assertEqual(count($entity->{$this->field_name}), $this->field['cardinality'], t('Revision %revision_id: expected number of values.', array('%revision_id' => $revision_id))); + $this->assertEqual(count($entity->{$this->field_name}[$language]), $this->field['cardinality'], t('Revision %revision_id: expected number of values.', array('%revision_id' => $revision_id))); for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { // The field value loaded matches the one inserted or updated. - $this->assertEqual($entity->{$this->field_name}[$delta]['value'], $values[$revision_id][$delta]['value'], t('Revision %revision_id: expected value %delta was found.', array('%revision_id' => $revision_id, '%delta' => $delta))); + $this->assertEqual($entity->{$this->field_name}[$language][$delta]['value'], $values[$revision_id][$delta]['value'], t('Revision %revision_id: expected value %delta was found.', array('%revision_id' => $revision_id, '%delta' => $delta))); // The value added in hook_field_load() is found. - $this->assertEqual($entity->{$this->field_name}[$delta]['additional_key'], 'additional_value', t('Revision %revision_id: extra information for value %delta was found', array('%revision_id' => $revision_id, '%delta' => $delta))); + $this->assertEqual($entity->{$this->field_name}[$language][$delta]['additional_key'], 'additional_value', t('Revision %revision_id: extra information for value %delta was found', array('%revision_id' => $revision_id, '%delta' => $delta))); } } } @@ -115,6 +116,7 @@ */ function testFieldAttachLoadMultiple() { $entity_type = 'test_entity'; + $language = FIELD_LANGUAGE_NONE; // Define 2 bundles. $bundles = array( @@ -157,7 +159,7 @@ $instances = field_info_instances($bundle); foreach ($instances as $field_name => $instance) { $values[$index][$field_name] = mt_rand(1, 127); - $entity->$field_name = array(array('value' => $values[$index][$field_name])); + $entity->$field_name = array($language => array(array('value' => $values[$index][$field_name]))); } field_attach_insert($entity_type, $entity); } @@ -168,17 +170,17 @@ $instances = field_info_instances($bundles[$index]); foreach ($instances as $field_name => $instance) { // The field value loaded matches the one inserted. - $this->assertEqual($entity->{$field_name}[0]['value'], $values[$index][$field_name], t('Entity %index: expected value was found.', array('%index' => $index))); + $this->assertEqual($entity->{$field_name}[$language][0]['value'], $values[$index][$field_name], t('Entity %index: expected value was found.', array('%index' => $index))); // The value added in hook_field_load() is found. - $this->assertEqual($entity->{$field_name}[0]['additional_key'], 'additional_value', t('Entity %index: extra information was found', array('%index' => $index))); + $this->assertEqual($entity->{$field_name}[$language][0]['additional_key'], 'additional_value', t('Entity %index: extra information was found', array('%index' => $index))); } } // Check that the single-field load option works. $entity = field_test_create_stub_entity(1, 1, $bundles[1]); field_attach_load($entity_type, array(1 => $entity), FIELD_LOAD_CURRENT, array('field_name' => $field_names[1])); - $this->assertEqual($entity->{$field_names[1]}[0]['value'], $values[1][$field_names[1]], t('Entity %index: expected value was found.', array('%index' => 1))); - $this->assertEqual($entity->{$field_names[1]}[0]['additional_key'], 'additional_value', t('Entity %index: extra information was found', array('%index' => 1))); + $this->assertEqual($entity->{$field_names[1]}[$language][0]['value'], $values[1][$field_names[1]], t('Entity %index: expected value was found.', array('%index' => 1))); + $this->assertEqual($entity->{$field_names[1]}[$language][0]['additional_key'], 'additional_value', t('Entity %index: extra information was found', array('%index' => 1))); $this->assert(!isset($entity->{$field_names[2]}), t('Entity %index: field %field_name is not loaded.', array('%index' => 2, '%field_name' => $field_names[2]))); $this->assert(!isset($entity->{$field_names[3]}), t('Entity %index: field %field_name is not loaded.', array('%index' => 3, '%field_name' => $field_names[3]))); } @@ -189,6 +191,7 @@ function testFieldAttachSaveMissingData() { $entity_type = 'test_entity'; $entity_init = field_test_create_stub_entity(); + $language = FIELD_LANGUAGE_NONE; // Insert: Field is missing. $entity = clone($entity_init); @@ -196,28 +199,28 @@ $entity = clone($entity_init); field_attach_load($entity_type, array($entity->ftid => $entity)); - $this->assertTrue(empty($entity->{$this->field_name}), t('Insert: missing field results in no value saved')); + $this->assertTrue(empty($entity->{$this->field_name}[$language]), t('Insert: missing field results in no value saved')); // Insert: Field is NULL. field_cache_clear(); $entity = clone($entity_init); - $entity->{$this->field_name} = NULL; + $entity->{$this->field_name}[$language] = NULL; field_attach_insert($entity_type, $entity); $entity = clone($entity_init); field_attach_load($entity_type, array($entity->ftid => $entity)); - $this->assertTrue(empty($entity->{$this->field_name}), t('Insert: NULL field results in no value saved')); + $this->assertTrue(empty($entity->{$this->field_name}[$language]), t('Insert: NULL field results in no value saved')); // Add some real data. field_cache_clear(); $entity = clone($entity_init); $values = $this->_generateTestFieldValues(1); - $entity->{$this->field_name} = $values; + $entity->{$this->field_name}[$language] = $values; field_attach_insert($entity_type, $entity); $entity = clone($entity_init); field_attach_load($entity_type, array($entity->ftid => $entity)); - $this->assertEqual($entity->{$this->field_name}, $values, t('Field data saved')); + $this->assertEqual($entity->{$this->field_name}[$language], $values, t('Field data saved')); // Update: Field is missing. Data should survive. field_cache_clear(); @@ -226,17 +229,17 @@ $entity = clone($entity_init); field_attach_load($entity_type, array($entity->ftid => $entity)); - $this->assertEqual($entity->{$this->field_name}, $values, t('Update: missing field leaves existing values in place')); + $this->assertEqual($entity->{$this->field_name}[$language], $values, t('Update: missing field leaves existing values in place')); // Update: Field is NULL. Data should be wiped. field_cache_clear(); $entity = clone($entity_init); - $entity->{$this->field_name} = NULL; + $entity->{$this->field_name}[$language] = NULL; field_attach_update($entity_type, $entity); $entity = clone($entity_init); field_attach_load($entity_type, array($entity->ftid => $entity)); - $this->assertTrue(empty($entity->{$this->field_name}), t('Update: NULL field removes existing values')); + $this->assertTrue(empty($entity->{$this->field_name}[$language]), t('Update: NULL field removes existing values')); } /** @@ -249,15 +252,16 @@ $entity_type = 'test_entity'; $entity_init = field_test_create_stub_entity(); + $language = FIELD_LANGUAGE_NONE; // Insert: Field is NULL. $entity = clone($entity_init); - $entity->{$this->field_name} = NULL; + $entity->{$this->field_name}[$language] = NULL; field_attach_insert($entity_type, $entity); $entity = clone($entity_init); field_attach_load($entity_type, array($entity->ftid => $entity)); - $this->assertTrue(empty($entity->{$this->field_name}), t('Insert: NULL field results in no value saved')); + $this->assertTrue(empty($entity->{$this->field_name}[$language]), t('Insert: NULL field results in no value saved')); // Insert: Field is missing. field_cache_clear(); @@ -267,7 +271,7 @@ $entity = clone($entity_init); field_attach_load($entity_type, array($entity->ftid => $entity)); $values = field_test_default_value($entity_type, $entity, $this->field, $this->instance); - $this->assertEqual($entity->{$this->field_name}, $values, t('Insert: missing field results in default value saved')); + $this->assertEqual($entity->{$this->field_name}[$language], $values, t('Insert: missing field results in default value saved')); } /** @@ -275,6 +279,7 @@ */ function testFieldAttachQuery() { $cardinality = $this->field['cardinality']; + $language = FIELD_LANGUAGE_NONE; // Create an additional bundle with an instance of the field. field_test_create_bundle('test_bundle_1', 'Test Bundle 1'); @@ -293,13 +298,13 @@ $value = mt_rand(1, 127); } while (in_array($value, $values)); $values[$delta] = $value; - $entities[1]->{$this->field_name}[$delta] = array('value' => $values[$delta]); + $entities[1]->{$this->field_name}[$language][$delta] = array('value' => $values[$delta]); } field_attach_insert($entity_types[1], $entities[1]); // Create second test object, sharing a value with the first one. $common_value = $values[$cardinality - 1]; - $entities[2]->{$this->field_name} = array(array('value' => $common_value)); + $entities[2]->{$this->field_name} = array($language => array(array('value' => $common_value))); field_attach_insert($entity_types[2], $entities[2]); // Query on the object's values. @@ -363,7 +368,7 @@ for ($i = 0; $i < 20; ++$i) { $offset_id += mt_rand(2, 5); $offset_entities[$offset_id] = field_test_create_stub_entity($offset_id, $offset_id, 'offset_bundle'); - $offset_entities[$offset_id]->{$this->field_name}[0] = array('value' => $offset_id); + $offset_entities[$offset_id]->{$this->field_name}[$language][0] = array('value' => $offset_id); field_attach_insert('test_entity', $offset_entities[$offset_id]); } @@ -396,19 +401,20 @@ // Create first object revision with random (distinct) values. $entity_type = 'test_entity'; $entities = array(1 => field_test_create_stub_entity(1, 1), 2 => field_test_create_stub_entity(1, 2)); + $language = FIELD_LANGUAGE_NONE; $values = array(); for ($delta = 0; $delta < $cardinality; $delta++) { do { $value = mt_rand(1, 127); } while (in_array($value, $values)); $values[$delta] = $value; - $entities[1]->{$this->field_name}[$delta] = array('value' => $values[$delta]); + $entities[1]->{$this->field_name}[$language][$delta] = array('value' => $values[$delta]); } field_attach_insert($entity_type, $entities[1]); // Create second object revision, sharing a value with the first one. $common_value = $values[$cardinality - 1]; - $entities[2]->{$this->field_name}[0] = array('value' => $common_value); + $entities[2]->{$this->field_name}[$language][0] = array('value' => $common_value); field_attach_update($entity_type, $entities[2]); // Query on the object's values. @@ -451,10 +457,11 @@ function testFieldAttachViewAndPreprocess() { $entity_type = 'test_entity'; $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $language = FIELD_LANGUAGE_NONE; // Populate values to be displayed. $values = $this->_generateTestFieldValues($this->field['cardinality']); - $entity->{$this->field_name} = $values; + $entity->{$this->field_name}[$language] = $values; // Simple formatter, label displayed. $formatter_setting = $this->randomName(); @@ -524,32 +531,45 @@ // TODO: // - check display order with several fields + + // Preprocess template. + $variables = array(); + field_attach_preprocess($entity_type, $entity, $entity->content, $variables); + $result = TRUE; + foreach ($values as $delta => $item) { + if ($variables[$this->field_name][$delta]['value'] !== $item['value']) { + $result = FALSE; + break; + } + } + $this->assertTrue($result, t('Variable $@field_name correctly populated', array('@field_name' => $this->field_name))); } function testFieldAttachDelete() { $entity_type = 'test_entity'; + $language = FIELD_LANGUAGE_NONE; $rev[0] = field_test_create_stub_entity(0, 0, $this->instance['bundle']); // Create revision 0 $values = $this->_generateTestFieldValues($this->field['cardinality']); - $rev[0]->{$this->field_name} = $values; + $rev[0]->{$this->field_name}[$language] = $values; field_attach_insert($entity_type, $rev[0]); // Create revision 1 $rev[1] = field_test_create_stub_entity(0, 1, $this->instance['bundle']); - $rev[1]->{$this->field_name} = $values; + $rev[1]->{$this->field_name}[$language] = $values; field_attach_update($entity_type, $rev[1]); // Create revision 2 $rev[2] = field_test_create_stub_entity(0, 2, $this->instance['bundle']); - $rev[2]->{$this->field_name} = $values; + $rev[2]->{$this->field_name}[$language] = $values; field_attach_update($entity_type, $rev[2]); // Confirm each revision loads foreach (array_keys($rev) as $vid) { $read = field_test_create_stub_entity(0, $vid, $this->instance['bundle']); field_attach_load_revision($entity_type, array(0 => $read)); - $this->assertEqual(count($read->{$this->field_name}), $this->field['cardinality'], "The test object revision $vid has {$this->field['cardinality']} values."); + $this->assertEqual(count($read->{$this->field_name}[$language]), $this->field['cardinality'], "The test object revision $vid has {$this->field['cardinality']} values."); } // Delete revision 1, confirm the other two still load. @@ -557,13 +577,13 @@ foreach (array(0, 2) as $vid) { $read = field_test_create_stub_entity(0, $vid, $this->instance['bundle']); field_attach_load_revision($entity_type, array(0 => $read)); - $this->assertEqual(count($read->{$this->field_name}), $this->field['cardinality'], "The test object revision $vid has {$this->field['cardinality']} values."); + $this->assertEqual(count($read->{$this->field_name}[$language]), $this->field['cardinality'], "The test object revision $vid has {$this->field['cardinality']} values."); } // Confirm the current revision still loads $read = field_test_create_stub_entity(0, 2, $this->instance['bundle']); field_attach_load($entity_type, array(0 => $read)); - $this->assertEqual(count($read->{$this->field_name}), $this->field['cardinality'], "The test object current revision has {$this->field['cardinality']} values."); + $this->assertEqual(count($read->{$this->field_name}[$language]), $this->field['cardinality'], "The test object current revision has {$this->field['cardinality']} values."); // Delete all field data, confirm nothing loads field_attach_delete($entity_type, $rev[2]); @@ -589,15 +609,16 @@ // Save an object with data in the field. $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $language = FIELD_LANGUAGE_NONE; $values = $this->_generateTestFieldValues($this->field['cardinality']); - $entity->{$this->field_name} = $values; + $entity->{$this->field_name}[$language] = $values; $entity_type = 'test_entity'; field_attach_insert($entity_type, $entity); // Verify the field data is present on load. $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); field_attach_load($entity_type, array(0 => $entity)); - $this->assertEqual(count($entity->{$this->field_name}), $this->field['cardinality'], "Data are retrieved for the new bundle"); + $this->assertEqual(count($entity->{$this->field_name}[$language]), $this->field['cardinality'], "Data are retrieved for the new bundle"); // Rename the bundle. This has to be initiated by the module so that its // hook_fieldable_info() is consistent. @@ -611,7 +632,7 @@ // Verify the field data is present on load. $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); field_attach_load($entity_type, array(0 => $entity)); - $this->assertEqual(count($entity->{$this->field_name}), $this->field['cardinality'], "Bundle name has been updated in the field storage"); + $this->assertEqual(count($entity->{$this->field_name}[$language]), $this->field['cardinality'], "Bundle name has been updated in the field storage"); } function testFieldAttachDeleteBundle() { @@ -643,17 +664,18 @@ // Save an object with data for both fields $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $language = FIELD_LANGUAGE_NONE; $values = $this->_generateTestFieldValues($this->field['cardinality']); - $entity->{$this->field_name} = $values; - $entity->{$field_name} = $this->_generateTestFieldValues(1); + $entity->{$this->field_name}[$language] = $values; + $entity->{$field_name}[$language] = $this->_generateTestFieldValues(1); $entity_type = 'test_entity'; field_attach_insert($entity_type, $entity); // Verify the fields are present on load $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); field_attach_load($entity_type, array(0 => $entity)); - $this->assertEqual(count($entity->{$this->field_name}), 4, "First field got loaded"); - $this->assertEqual(count($entity->{$field_name}), 1, "Second field got loaded"); + $this->assertEqual(count($entity->{$this->field_name}[$language]), 4, "First field got loaded"); + $this->assertEqual(count($entity->{$field_name}[$language]), 1, "Second field got loaded"); // Delete the bundle. This has to be initiated by the module so that its // hook_fieldable_info() is consistent. @@ -662,8 +684,8 @@ // Verify no data gets loaded $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); field_attach_load($entity_type, array(0 => $entity)); - $this->assertFalse(isset($entity->{$this->field_name}), "No data for first field"); - $this->assertFalse(isset($entity->{$field_name}), "No data for second field"); + $this->assertFalse(isset($entity->{$this->field_name}[$language]), "No data for first field"); + $this->assertFalse(isset($entity->{$field_name}[$language]), "No data for second field"); // Verify that the instances are gone $this->assertFalse(field_read_instance($this->field_name, $this->instance['bundle']), "First field is deleted"); @@ -676,6 +698,7 @@ function testFieldAttachCache() { // Initialize random values and a test entity. $entity_init = field_test_create_stub_entity(1, 1, $this->instance['bundle']); + $language = FIELD_LANGUAGE_NONE; $values = $this->_generateTestFieldValues($this->field['cardinality']); $noncached_type = 'test_entity'; @@ -690,7 +713,7 @@ // Save, and check that no cache entry is present. $entity = clone($entity_init); - $entity->{$this->field_name} = $values; + $entity->{$this->field_name}[$language] = $values; field_attach_insert($noncached_type, $entity); $this->assertFalse(cache_get($cid, 'cache_field'), t('Non-cached: no cache entry on insert')); @@ -708,7 +731,7 @@ // Save, and check that no cache entry is present. $entity = clone($entity_init); - $entity->{$this->field_name} = $values; + $entity->{$this->field_name}[$language] = $values; field_attach_insert($cached_type, $entity); $this->assertFalse(cache_get($cid, 'cache_field'), t('Cached: no cache entry on insert')); @@ -716,12 +739,12 @@ $entity = clone($entity_init); field_attach_load($cached_type, array($entity->ftid => $entity)); $cache = cache_get($cid, 'cache_field'); - $this->assertEqual($cache->data[$this->field_name], $values, t('Cached: correct cache entry on load')); + $this->assertEqual($cache->data[$this->field_name][$language], $values, t('Cached: correct cache entry on load')); // Update with different values, and check that the cache entry is wiped. $values = $this->_generateTestFieldValues($this->field['cardinality']); $entity = clone($entity_init); - $entity->{$this->field_name} = $values; + $entity->{$this->field_name}[$language] = $values; field_attach_update($cached_type, $entity); $this->assertFalse(cache_get($cid, 'cache_field'), t('Cached: no cache entry on update')); @@ -729,13 +752,13 @@ $entity = clone($entity_init); field_attach_load($cached_type, array($entity->ftid => $entity)); $cache = cache_get($cid, 'cache_field'); - $this->assertEqual($cache->data[$this->field_name], $values, t('Cached: correct cache entry on load')); + $this->assertEqual($cache->data[$this->field_name][$language], $values, t('Cached: correct cache entry on load')); // Create a new revision, and check that the cache entry is wiped. $entity_init = field_test_create_stub_entity(1, 2, $this->instance['bundle']); $values = $this->_generateTestFieldValues($this->field['cardinality']); $entity = clone($entity_init); - $entity->{$this->field_name} = $values; + $entity->{$this->field_name}[$language] = $values; field_attach_update($cached_type, $entity); $cache = cache_get($cid, 'cache_field'); $this->assertFalse(cache_get($cid, 'cache_field'), t('Cached: no cache entry on new revision creation')); @@ -744,7 +767,7 @@ $entity = clone($entity_init); field_attach_load($cached_type, array($entity->ftid => $entity)); $cache = cache_get($cid, 'cache_field'); - $this->assertEqual($cache->data[$this->field_name], $values, t('Cached: correct cache entry on load')); + $this->assertEqual($cache->data[$this->field_name][$language], $values, t('Cached: correct cache entry on load')); // Delete, and check that the cache entry is wiped. field_attach_delete($cached_type, $entity); @@ -756,6 +779,7 @@ function testFieldAttachValidate() { $entity_type = 'test_entity'; $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $language = FIELD_LANGUAGE_NONE; // Set up values to generate errors $values = array(); @@ -765,7 +789,7 @@ } // Arrange for item 1 not to generate an error $values[1]['value'] = 1; - $entity->{$this->field_name} = $values; + $entity->{$this->field_name}[$language] = $values; try { field_attach_validate($entity_type, $entity); @@ -776,15 +800,15 @@ foreach ($values as $delta => $value) { if ($value['value'] != 1) { - $this->assertIdentical($errors[$this->field_name][$delta][0]['error'], 'field_test_invalid', "Error set on value $delta"); - $this->assertEqual(count($errors[$this->field_name][$delta]), 1, "Only one error set on value $delta"); - unset($errors[$this->field_name][$delta]); + $this->assertIdentical($errors[$this->field_name][$language][$delta][0]['error'], 'field_test_invalid', "Error set on value $delta"); + $this->assertEqual(count($errors[$this->field_name][$language][$delta]), 1, "Only one error set on value $delta"); + unset($errors[$this->field_name][$language][$delta]); } else { - $this->assertFalse(isset($errors[$this->field_name][$delta]), "No error set on value $delta"); + $this->assertFalse(isset($errors[$this->field_name][$language][$delta]), "No error set on value $delta"); } } - $this->assertEqual(count($errors[$this->field_name]), 0, 'No extraneous errors set'); + $this->assertEqual(count($errors[$this->field_name][$language]), 0, 'No extraneous errors set'); } // Validate that FAPI elements are generated. This could be much @@ -796,10 +820,11 @@ $form = $form_state = array(); field_attach_form($entity_type, $entity, $form, $form_state); - $this->assertEqual($form[$this->field_name]['#title'], $this->instance['label'], "Form title is {$this->instance['label']}"); + $language = FIELD_LANGUAGE_NONE; + $this->assertEqual($form[$this->field_name][$language]['#title'], $this->instance['label'], "Form title is {$this->instance['label']}"); for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { // field_test_widget uses 'textfield' - $this->assertEqual($form[$this->field_name][$delta]['value']['#type'], 'textfield', "Form delta $delta widget is textfield"); + $this->assertEqual($form[$this->field_name][$language][$delta]['value']['#type'], 'textfield', "Form delta $delta widget is textfield"); } } @@ -826,7 +851,8 @@ // Leave an empty value. 'field_test' fields are empty if empty(). $values[1]['value'] = 0; - $form_state['values'] = array($this->field_name => $values); + $language = FIELD_LANGUAGE_NONE; + $form_state['values'] = array($this->field_name => array($language => $values)); field_attach_submit($entity_type, $entity, $form, $form_state); asort($weights); @@ -836,7 +862,7 @@ $expected_values[] = array('value' => $values[$key]['value']); } } - $this->assertIdentical($entity->{$this->field_name}, $expected_values, 'Submit filters empty values'); + $this->assertIdentical($entity->{$this->field_name}[$language], $expected_values, 'Submit filters empty values'); } /** @@ -1095,45 +1121,46 @@ $this->instance['field_name'] = $this->field_name; field_create_field($this->field); field_create_instance($this->instance); + $language = FIELD_LANGUAGE_NONE; // Display creation form. $this->drupalGet('test-entity/add/test-bundle'); - $this->assertFieldByName($this->field_name . '[0][value]', '', 'Widget is displayed'); - $this->assertNoField($this->field_name . '[1][value]', 'No extraneous widget is displayed'); + $this->assertFieldByName($this->field_name . '[' . $language . '][0][value]', '', 'Widget is displayed'); + $this->assertNoField($this->field_name . '[' . $language . '][1][value]', 'No extraneous widget is displayed'); // TODO : check that the widget is populated with default value ? // Submit with invalid value (field-level validation). - $edit = array($this->field_name . '[0][value]' => -1); + $edit = array($this->field_name . '[' . $language . '][0][value]' => -1); $this->drupalPost(NULL, $edit, t('Save')); $this->assertRaw(t('%name does not accept the value -1.', array('%name' => $this->instance['label'])), 'Field validation fails with invalid input.'); // TODO : check that the correct field is flagged for error. // Create an entity $value = mt_rand(1, 127); - $edit = array($this->field_name . '[0][value]' => $value); + $edit = array($this->field_name . '[' . $language . '][0][value]' => $value); $this->drupalPost(NULL, $edit, t('Save')); preg_match('|test-entity/(\d+)/edit|', $this->url, $match); $id = $match[1]; $this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), 'Entity was created'); $entity = field_test_entity_load($id); - $this->assertEqual($entity->{$this->field_name}[0]['value'], $value, 'Field value was saved'); + $this->assertEqual($entity->{$this->field_name}[$language][0]['value'], $value, 'Field value was saved'); // Display edit form. $this->drupalGet('test-entity/' . $id . '/edit'); - $this->assertFieldByName($this->field_name . '[0][value]', $value, 'Widget is displayed with the correct default value'); - $this->assertNoField($this->field_name . '[1][value]', 'No extraneous widget is displayed'); + $this->assertFieldByName($this->field_name . '[' . $language . '][0][value]', $value, 'Widget is displayed with the correct default value'); + $this->assertNoField($this->field_name . '[' . $language . '][1][value]', 'No extraneous widget is displayed'); // Update the entity. $value = mt_rand(1, 127); - $edit = array($this->field_name . '[0][value]' => $value); + $edit = array($this->field_name . '[' . $language . '][0][value]' => $value); $this->drupalPost(NULL, $edit, t('Save')); $this->assertRaw(t('test_entity @id has been updated.', array('@id' => $id)), 'Entity was updated'); $entity = field_test_entity_load($id); - $this->assertEqual($entity->{$this->field_name}[0]['value'], $value, 'Field value was updated'); + $this->assertEqual($entity->{$this->field_name}[$language][0]['value'], $value, 'Field value was updated'); // Empty the field. $value = ''; - $edit = array($this->field_name . '[0][value]' => $value); + $edit = array($this->field_name . '[' . $language . '][0][value]' => $value); $this->drupalPost('test-entity/' . $id . '/edit', $edit, t('Save')); $this->assertRaw(t('test_entity @id has been updated.', array('@id' => $id)), 'Entity was updated'); $entity = field_test_entity_load($id); @@ -1148,6 +1175,7 @@ $this->instance['required'] = TRUE; field_create_field($this->field); field_create_instance($this->instance); + $language = FIELD_LANGUAGE_NONE; // Submit with missing required value. $edit = array(); @@ -1156,17 +1184,17 @@ // Create an entity $value = mt_rand(1, 127); - $edit = array($this->field_name . '[0][value]' => $value); + $edit = array($this->field_name . '[' . $language . '][0][value]' => $value); $this->drupalPost(NULL, $edit, t('Save')); preg_match('|test-entity/(\d+)/edit|', $this->url, $match); $id = $match[1]; $this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), 'Entity was created'); $entity = field_test_entity_load($id); - $this->assertEqual($entity->{$this->field_name}[0]['value'], $value, 'Field value was saved'); + $this->assertEqual($entity->{$this->field_name}[$language][0]['value'], $value, 'Field value was saved'); // Edit with missing required value. $value = ''; - $edit = array($this->field_name . '[0][value]' => $value); + $edit = array($this->field_name . '[' . $language . '][0][value]' => $value); $this->drupalPost('test-entity/' . $id . '/edit', $edit, t('Save')); $this->assertRaw(t('!name field is required.', array('!name' => $this->instance['label'])), 'Required field with no value fails validation'); } @@ -1185,17 +1213,18 @@ $this->instance['field_name'] = $this->field_name; field_create_field($this->field); field_create_instance($this->instance); + $language = FIELD_LANGUAGE_NONE; // Display creation form -> 1 widget. $this->drupalGet('test-entity/add/test-bundle'); - $this->assertFieldByName($this->field_name . '[0][value]', '', 'Widget 1 is displayed'); - $this->assertNoField($this->field_name . '[1][value]', 'No extraneous widget is displayed'); + $this->assertFieldByName($this->field_name . '[' . $language . '][0][value]', '', 'Widget 1 is displayed'); + $this->assertNoField($this->field_name . '[' . $language . '][1][value]', 'No extraneous widget is displayed'); // Press 'add more' button -> 2 widgets. $this->drupalPost(NULL, array(), t('Add another item')); - $this->assertFieldByName($this->field_name . '[0][value]', '', 'Widget 1 is displayed'); - $this->assertFieldByName($this->field_name . '[1][value]', '', 'New widget is displayed'); - $this->assertNoField($this->field_name . '[2][value]', 'No extraneous widget is displayed'); + $this->assertFieldByName($this->field_name . '[' . $language . '][0][value]', '', 'Widget 1 is displayed'); + $this->assertFieldByName($this->field_name . '[' . $language . '][1][value]', '', 'New widget is displayed'); + $this->assertNoField($this->field_name . '[' . $language . '][2][value]', 'No extraneous widget is displayed'); // TODO : check that non-field inpurs are preserved ('title')... // Yet another time so that we can play with more values -> 3 widgets. @@ -1212,8 +1241,8 @@ } while (in_array($weight, $weights)); $weights[] = $weight; $value = mt_rand(1, 127); - $edit["$this->field_name[$delta][value]"] = $value; - $edit["$this->field_name[$delta][_weight]"] = $weight; + $edit["$this->field_name[$language][$delta][value]"] = $value; + $edit["$this->field_name[$language][$delta][_weight]"] = $weight; // We'll need three slightly different formats to check the values. $values[$weight] = $value; $field_values[$weight]['value'] = (string)$value; @@ -1225,15 +1254,15 @@ ksort($values); $values = array_values($values); for ($delta = 0; $delta <= $delta_range; $delta++) { - $this->assertFieldByName("$this->field_name[$delta][value]", $values[$delta], "Widget $delta is displayed and has the right value"); - $this->assertFieldByName("$this->field_name[$delta][_weight]", $delta, "Widget $delta has the right weight"); + $this->assertFieldByName("$this->field_name[$language][$delta][value]", $values[$delta], "Widget $delta is displayed and has the right value"); + $this->assertFieldByName("$this->field_name[$language][$delta][_weight]", $delta, "Widget $delta has the right weight"); } ksort($pattern); $pattern = implode('.*', array_values($pattern)); $this->assertPattern("|$pattern|s", 'Widgets are displayed in the correct order'); - $this->assertFieldByName("$this->field_name[$delta][value]", '', "New widget is displayed"); - $this->assertFieldByName("$this->field_name[$delta][_weight]", $delta, "New widget has the right weight"); - $this->assertNoField("$this->field_name[" . ($delta + 1) . '][value]', 'No extraneous widget is displayed'); + $this->assertFieldByName("$this->field_name[$language][$delta][value]", '', "New widget is displayed"); + $this->assertFieldByName("$this->field_name[$language][$delta][_weight]", $delta, "New widget has the right weight"); + $this->assertNoField("$this->field_name[$language][" . ($delta + 1) . '][value]', 'No extraneous widget is displayed'); // Submit the form and create the entity. $this->drupalPost(NULL, $edit, t('Save')); @@ -1243,7 +1272,7 @@ $entity = field_test_entity_load($id); ksort($field_values); $field_values = array_values($field_values); - $this->assertIdentical($entity->{$this->field_name}, $field_values, 'Field values were saved in the correct order'); + $this->assertIdentical($entity->{$this->field_name}[$language], $field_values, 'Field values were saved in the correct order'); // Display edit form: check that the expected number of widgets is // displayed, with correct values change values, reorder, leave an empty @@ -1265,6 +1294,7 @@ $this->instance['field_name'] = $this->field_name; field_create_field($this->field); field_create_instance($this->instance); + $language = FIELD_LANGUAGE_NONE; // Display creation form -> 1 widget. $this->drupalGet('test-entity/add/test-bundle'); @@ -1286,8 +1316,8 @@ } while (in_array($weight, $weights)); $weights[] = $weight; $value = mt_rand(1, 127); - $edit["$this->field_name[$delta][value]"] = $value; - $edit["$this->field_name[$delta][_weight]"] = $weight; + $edit["$this->field_name[$language][$delta][value]"] = $value; + $edit["$this->field_name[$language][$delta][_weight]"] = $weight; // We'll need three slightly different formats to check the values. $values[$weight] = $value; $field_values[$weight]['value'] = (string)$value; @@ -1300,15 +1330,15 @@ ksort($values); $values = array_values($values); for ($delta = 0; $delta <= $delta_range; $delta++) { - $this->assertFieldByName("$this->field_name[$delta][value]", $values[$delta], "Widget $delta is displayed and has the right value"); - $this->assertFieldByName("$this->field_name[$delta][_weight]", $delta, "Widget $delta has the right weight"); + $this->assertFieldByName("$this->field_name[$language][$delta][value]", $values[$delta], "Widget $delta is displayed and has the right value"); + $this->assertFieldByName("$this->field_name[$language][$delta][_weight]", $delta, "Widget $delta has the right weight"); } ksort($pattern); $pattern = implode('.*', array_values($pattern)); $this->assertPattern("|$pattern|s", 'Widgets are displayed in the correct order'); - $this->assertFieldByName("$this->field_name[$delta][value]", '', "New widget is displayed"); - $this->assertFieldByName("$this->field_name[$delta][_weight]", $delta, "New widget has the right weight"); - $this->assertNoField("$this->field_name[" . ($delta + 1) . '][value]', 'No extraneous widget is displayed'); + $this->assertFieldByName("$this->field_name[$language][$delta][value]", '', "New widget is displayed"); + $this->assertFieldByName("$this->field_name[$language][$delta][_weight]", $delta, "New widget has the right weight"); + $this->assertNoField("$this->field_name[$language][" . ($delta + 1) . '][value]', 'No extraneous widget is displayed'); } /** @@ -1558,17 +1588,18 @@ // Save an object with data for the field $entity = field_test_create_stub_entity(0, 0, $instance['bundle']); + $language = FIELD_LANGUAGE_NONE; $values[0]['value'] = mt_rand(1, 127); - $entity->{$field['field_name']} = $values; + $entity->{$field['field_name']}[$language] = $values; $entity_type = 'test_entity'; field_attach_insert($entity_type, $entity); // Verify the field is present on load $entity = field_test_create_stub_entity(0, 0, $this->instance_definition['bundle']); field_attach_load($entity_type, array(0 => $entity)); - $this->assertIdentical(count($entity->{$field['field_name']}), count($values), "Data in previously deleted field saves and loads correctly"); + $this->assertIdentical(count($entity->{$field['field_name']}[$language]), count($values), "Data in previously deleted field saves and loads correctly"); foreach ($values as $delta => $value) { - $this->assertEqual($entity->{$field['field_name']}[$delta]['value'], $values[$delta]['value'], "Data in previously deleted field saves and loads correctly"); + $this->assertEqual($entity->{$field['field_name']}[$language][$delta]['value'], $values[$delta]['value'], "Data in previously deleted field saves and loads correctly"); } } } @@ -1755,3 +1786,229 @@ $this->assertTrue(!empty($another_instance) && empty($another_instance['deleted']), t('A non-deleted field instance is not marked for deletion.')); } } + +/** + * Unit test class for the multilanguage fields logic. + * + * The following tests will check the multilanguage logic of + * _field_invoke and that only the correct values are returned + * by field_multilingual_available_languages. + */ +class FieldTranslationsTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => t('Field translations tests'), + 'description' => t("Test multilanguage fields logic."), + 'group' => t('Field') + ); + } + + function setUp() { + parent::setUp('locale', 'field_test'); + + $this->field_name = drupal_strtolower($this->randomName() . '_field_name'); + + $this->obj_type = 'test_entity'; + + $this->field = array( + 'field_name' => $this->field_name, + 'type' => 'test_field', + 'cardinality' => 4, + 'translatable' => TRUE, + 'settings' => array( + 'test_hook_in' => FALSE, + ), + ); + field_create_field($this->field); + + $this->instance = array( + 'field_name' => $this->field_name, + 'bundle' => 'test_bundle', + 'label' => $this->randomName() . '_label', + 'description' => $this->randomName() . '_description', + 'weight' => mt_rand(0, 127), + 'settings' => array( + 'test_instance_setting' => $this->randomName(), + ), + 'widget' => array( + 'type' => 'test_field_widget', + 'label' => 'Test Field', + 'settings' => array( + 'test_widget_setting' => $this->randomName(), + ) + ) + ); + field_create_instance($this->instance); + + for ($i = 0; $i < 3; ++$i) { + locale_inc_callback('locale_add_language', 'l'.$i, $this->randomString(), $this->randomString()); + } + } + + /** + * Check that that only the correct values are returned by field_multilingual_available_languages. + */ + function testFieldAvailableLanguages() { + // Test translatable fieldable info. + $field = $this->field; + $field['field_name'] .= '_untranslatable'; + $language = language_default(); + $suggested_languages = array($language->language); + $available_languages = field_multilingual_available_languages($this->obj_type, $field); + $this->assertTrue(count($available_languages) == 1 && $available_languages[0] === FIELD_LANGUAGE_NONE, t('Untranslatable entity: suggested language ignored')); + + // Enable field translations for the entity. + field_test_fieldable_info_translatable('test_entity', TRUE); + + // Test hook_field_languages invocation on a translatable field. + $this->field['settings']['test_hook_in'] = TRUE; + $enabled_languages = array_keys(language_list('language', LANGUAGE_TYPE_CONTENT)); + $available_languages = field_multilingual_available_languages($this->obj_type, $this->field); + $this->assertTrue(in_array(FIELD_LANGUAGE_NONE, $available_languages), t('%language is an available language', array('%language' => FIELD_LANGUAGE_NONE))); + foreach ($available_languages as $delta => $language) { + if ($language != FIELD_LANGUAGE_NONE) { + $this->assertTrue(in_array($language, $enabled_languages), t('%language is an enabled language', array('%language' => $language))); + } + } + $this->assertFalse(in_array('xx', $available_languages), t('No invalid language was made available')); + $this->assertTrue(count($available_languages) == count($enabled_languages), t('An enabled language was successfully made unavailable')); + + // Test field_multilingual_available_languages behavior for untranslatable fields. + $this->field['translatable'] = FALSE; + $this->field_name = $this->field['field_name'] = $this->instance['field_name'] = drupal_strtolower($this->randomName() . '_field_name'); + $available_languages = field_multilingual_available_languages($this->obj_type, $this->field); + $this->assertTrue(count($available_languages) == 1 && $available_languages[0] === FIELD_LANGUAGE_NONE, t('For untranslatable fields only neutral language is available')); + + // Test language suggestions. + $this->field['settings']['test_hook_in'] = FALSE; + $this->field['translatable'] = TRUE; + $this->field_name = $this->field['field_name'] = $this->instance['field_name'] = drupal_strtolower($this->randomName() . '_field_name'); + $suggested_languages = array(); + $lang_count = mt_rand(1, count($enabled_languages) - 1); + for ($i = 0; $i < $lang_count; ++$i) { + do { + $language = $enabled_languages[mt_rand(0, $lang_count)]; + } + while (in_array($language, $suggested_languages)); + $suggested_languages[] = $language; + } + $available_languages = field_multilingual_available_languages($this->obj_type, $this->field, $suggested_languages); + $this->assertEqual(count($available_languages), count($suggested_languages), t('Suggested languages were succesully made available')); + foreach ($available_languages as $language) { + $this->assertTrue(in_array($language, $available_languages), t('Suggested language %language is available', array('%language' => $language))); + } + $this->field_name = $this->field['field_name'] = $this->instance['field_name'] = drupal_strtolower($this->randomName() . '_field_name'); + $suggested_languages = array('xx'); + $available_languages = field_multilingual_available_languages($this->obj_type, $this->field, $suggested_languages); + $this->assertTrue(empty($available_languages), t('An invalid suggested language was not made available')); + } + + /** + * Test the multilanguage logic of _field_invoke. + */ + function testFieldInvoke() { + $entity_type = 'test_entity'; + $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + + // Populate some extra languages to check if _field_invoke correctly uses + // the result of field_multilingual_available_languages. + $values = array(); + $extra_languages = mt_rand(1, 4); + $languages = $available_languages = field_multilingual_available_languages($this->obj_type, $this->field); + for ($i = 0; $i < $extra_languages; ++$i) { + $languages[] = $this->randomString(2); + } + + // For each given language provide some random values. + foreach ($languages as $language) { + for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { + $values[$language][$delta]['value'] = mt_rand(1, 127); + } + } + $entity->{$this->field_name} = $values; + + $results = _field_invoke('test_op', $entity_type, $entity); + foreach ($results as $language => $result) { + $hash = md5(serialize(array($entity_type, $entity, $this->field_name, $language, $values[$language]))); + // Check if the parameters passed to _field_invoke were correctly forwarded to the callback function. + $this->assertEqual($hash, $result, t('The result for %language is correctly stored', array('%language' => $language))); + } + $this->assertEqual(count($results), count($available_languages), t('No unavailable language has been processed')); + } + + /** + * Test the multilanguage logic of _field_invoke_multiple. + */ + function testFieldInvokeMultiple() { + $values = array(); + $entities = array(); + $entity_type = 'test_entity'; + $entity_count = mt_rand(1, 5); + $available_languages = field_multilingual_available_languages($this->obj_type, $this->field); + + for ($id = 1; $id <= $entity_count; ++$id) { + $entity = field_test_create_stub_entity($id, $id, $this->instance['bundle']); + $languages = $available_languages; + + // Populate some extra languages to check if _field_invoke correctly uses + // the result of field_multilingual_available_languages. + $extra_languages = mt_rand(1, 4); + for ($i = 0; $i < $extra_languages; ++$i) { + $languages[] = $this->randomString(2); + } + + // For each given language provide some random values. + foreach ($languages as $language) { + for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { + $values[$id][$language][$delta]['value'] = mt_rand(1, 127); + } + } + $entity->{$this->field_name} = $values[$id]; + $entities[$id] = $entity; + } + + $grouped_results = _field_invoke_multiple('test_op_multiple', $entity_type, $entities); + foreach ($grouped_results as $id => $results) { + foreach ($results as $language => $result) { + $hash = md5(serialize(array($entity_type, $entities[$id], $this->field_name, $language, $values[$id][$language]))); + // Check if the parameters passed to _field_invoke were correctly forwarded to the callback function. + $this->assertEqual($hash, $result, t('The result for object %id/%language is correctly stored', array('%id' => $id, '%language' => $language))); + } + $this->assertEqual(count($results), count($available_languages), t('No unavailable language has been processed for object %id', array('%id' => $id))); + } + } + + /** + * Test translatable fields storage/retrieval. + */ + function testTranslatableFieldSaveLoad() { + // Enable field translations for nodes. + field_test_fieldable_info_translatable('node', TRUE); + $obj_info = field_info_fieldable_types('node'); + $this->assertTrue(count($obj_info['translation_handlers']), t('Nodes are translatable')); + + // Prepare the field translations. + $eid = $evid = 1; + $obj_type = 'test_entity'; + $object = field_test_create_stub_entity($eid, $evid, $this->instance['bundle']); + $field_translations = array(); + foreach (field_multilingual_available_languages($obj_type, $this->field) as $language) { + $field_translations[$language] = FieldAttachTestCase::_generateTestFieldValues($this->field['cardinality']); + } + + // Save and reload the field translations. + $object->{$this->field_name} = $field_translations; + field_attach_insert($obj_type, $object); + unset($object->{$this->field_name}); + field_attach_load($obj_type, array($eid => $object)); + + // Check if the correct values were saved/loaded. + foreach ($field_translations as $language => $items) { + $result = TRUE; + foreach ($items as $delta => $item) { + $result = $result && $item['value'] == $object->{$this->field_name}[$language][$delta]['value']; + } + $this->assertTrue($result, t('%language translation correctly handled', array('%language' => $language))); + } + } +} Index: modules/field/field.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.module,v retrieving revision 1.21 diff -u -r1.21 field.module --- modules/field/field.module 2 Aug 2009 11:24:21 -0000 1.21 +++ modules/field/field.module 3 Aug 2009 18:18:16 -0000 @@ -12,6 +12,7 @@ */ require(DRUPAL_ROOT . '/modules/field/field.crud.inc'); require(DRUPAL_ROOT . '/modules/field/field.info.inc'); +require(DRUPAL_ROOT . '/modules/field/field.multilingual.inc'); require(DRUPAL_ROOT . '/modules/field/field.attach.inc'); /** @@ -64,6 +65,12 @@ define('FIELD_CARDINALITY_UNLIMITED', -1); /** + * The language code assigned to untranslatable fields. + * (ISO639-2 No linguistic content / Not applicable) + */ +define('FIELD_LANGUAGE_NONE', 'zxx'); + +/** * TODO */ define('FIELD_BEHAVIOR_NONE', 0x0001); @@ -605,6 +612,8 @@ 'label' => check_plain(t($instance['label'])), 'label_display' => $element['#label_display'], 'field_empty' => $field_empty, + 'field_language' => $element['#language'], + 'field_translatable' => $field['translatable'], 'template_files' => array( 'field', 'field-' . $element['#field_name'], Index: modules/field/field.default.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.default.inc,v retrieving revision 1.12 diff -u -r1.12 field.default.inc --- modules/field/field.default.inc 2 Aug 2009 11:24:21 -0000 1.12 +++ modules/field/field.default.inc 3 Aug 2009 18:18:15 -0000 @@ -11,17 +11,17 @@ * the corresponding field_attach_[operation]() function. */ -function field_default_extract_form_values($obj_type, $object, $field, $instance, &$items, $form, &$form_state) { +function field_default_extract_form_values($obj_type, $object, $field, $instance, $language, &$items, $form, &$form_state) { $field_name = $field['field_name']; - if (isset($form_state['values'][$field_name])) { - $items = $form_state['values'][$field_name]; + if (isset($form_state['values'][$field_name][$language])) { + $items = $form_state['values'][$field_name][$language]; // Remove the 'value' of the 'add more' button. unset($items[$field_name . '_add_more']); } } -function field_default_submit($obj_type, $object, $field, $instance, &$items, $form, &$form_state) { +function field_default_submit($obj_type, $object, $field, $instance, $language, &$items, $form, &$form_state) { $field_name = $field['field_name']; // Reorder items to account for drag-n-drop reordering. @@ -40,13 +40,18 @@ * This can happen with programmatic saves, or on form-based creation where * the current user doesn't have 'edit' permission for the field. */ -function field_default_insert($obj_type, $object, $field, $instance, &$items) { +function field_default_insert($obj_type, $object, $field, $instance, $language, &$items) { // _field_invoke() populates $items with an empty array if the $object has no // entry for the field, so we check on the $object itself. - if (!property_exists($object, $field['field_name']) && !empty($instance['default_value_function'])) { + // We also check that the current field translation is actually defined before + // assigning it a default value, this way we ensure that only the intended languages + // get a default value, otherwise we could have default values for not yet open + // languages. + if (!empty($instance['default_value_function']) && (!property_exists($object, $field['field_name']) || + (isset($object->{$field['field_name']}[$language]) && count($object->{$field['field_name']}[$language]) == 0))) { $function = $instance['default_value_function']; if (drupal_function_exists($function)) { - $items = $function($obj_type, $object, $field, $instance); + $items = $function($obj_type, $object, $field, $instance, $language); } } } @@ -108,7 +113,7 @@ * ), * ); */ -function field_default_view($obj_type, $object, $field, $instance, $items, $build_mode) { +function field_default_view($obj_type, $object, $field, $instance, $language, $items, $build_mode) { list($id, $vid, $bundle) = field_attach_extract_ids($obj_type, $object); $addition = array(); @@ -138,6 +143,7 @@ '#label_display' => $label_display, '#build_mode' => $build_mode, '#single' => $single, + '#language' => $language, 'items' => array(), ); @@ -173,7 +179,7 @@ return $addition; } -function field_default_prepare_translation($obj_type, $object, $field, $instance, &$items) { +function field_default_prepare_translation($obj_type, $object, $field, $instance, $language, &$items) { $addition = array(); if (isset($object->translation_source->$field['field_name'])) { $addition[$field['field_name']] = $object->translation_source->$field['field_name']; Index: modules/field/field.info =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.info,v retrieving revision 1.4 diff -u -r1.4 field.info --- modules/field/field.info 8 Jun 2009 09:23:51 -0000 1.4 +++ modules/field/field.info 3 Aug 2009 18:18:16 -0000 @@ -9,6 +9,7 @@ files[] = field.crud.inc files[] = field.info.inc files[] = field.default.inc +files[] = field.multilingual.inc files[] = field.attach.inc files[] = field.form.inc files[] = field.test Index: modules/field/field.form.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.form.inc,v retrieving revision 1.12 diff -u -r1.12 field.form.inc --- modules/field/field.form.inc 27 Jul 2009 20:19:20 -0000 1.12 +++ modules/field/field.form.inc 3 Aug 2009 18:18:16 -0000 @@ -9,7 +9,7 @@ /** * Create a separate form element for each field. */ -function field_default_form($obj_type, $object, $field, $instance, $items, &$form, &$form_state, $get_delta = NULL) { +function field_default_form($obj_type, $object, $field, $instance, $language, $items, &$form, &$form_state, $get_delta = NULL) { // This could be called with no object, as when a UI module creates a // dummy form to set default values. if ($object) { @@ -52,7 +52,7 @@ // and we are displaying an individual element, process the multiple value // form. if (!isset($get_delta) && field_behaviors_widget('multiple values', $instance) == FIELD_BEHAVIOR_DEFAULT) { - $form_element = field_multiple_value_form($field, $instance, $items, $form, $form_state); + $form_element = field_multiple_value_form($field, $instance, $language, $items, $form, $form_state); } // If the widget is handling multiple values (e.g Options), // or if we are displaying an individual element, just get a single form @@ -93,7 +93,15 @@ '#weight' => $instance['weight'], ); - $addition[$field['field_name']] = array_merge($form_element, $defaults); + $form_element = array_merge($form_element, $defaults); + + $addition[$field['field_name']] = array( + '#tree' => TRUE, + '#weight' => $form_element['#weight'], + '#language' => $language, + $language => $form_element, + ); + $form['#fields'][$field['field_name']]['form_path'] = array($field['field_name']); } @@ -108,7 +116,7 @@ * - AHAH-'add more' button * - drag-n-drop value reordering */ -function field_multiple_value_form($field, $instance, $items, &$form, &$form_state) { +function field_multiple_value_form($field, $instance, $language, $items, &$form, &$form_state) { $field = field_info_field($instance['field_name']); $field_name = $field['field_name']; @@ -202,9 +210,11 @@ '#field_name' => $field_name, '#bundle' => $instance['bundle'], '#attributes' => array('class' => 'field-add-more-submit'), + '#language' => $language, ); } } + return $form_element; } @@ -281,9 +291,9 @@ /** * Transfer field-level validation errors to widgets. */ -function field_default_form_errors($obj_type, $object, $field, $instance, $items, $form, $errors) { +function field_default_form_errors($obj_type, $object, $field, $instance, $language, $items, $form, $errors) { $field_name = $field['field_name']; - if (!empty($errors[$field_name])) { + if (!empty($errors[$field_name][$language])) { $function = $instance['widget']['module'] . '_field_widget_error'; $function_exists = drupal_function_exists($function); @@ -295,10 +305,10 @@ } $multiple_widget = field_behaviors_widget('multiple values', $instance) != FIELD_BEHAVIOR_DEFAULT; - foreach ($errors[$field_name] as $delta => $delta_errors) { + foreach ($errors[$field_name][$language] as $delta => $delta_errors) { // For multiple single-value widgets, pass errors by delta. // For a multiple-value widget, all errors are passed to the main widget. - $error_element = $multiple_widget ? $element : $element[$delta]; + $error_element = $multiple_widget ? $element[$language] : $element[$language][$delta]; foreach ($delta_errors as $error) { if ($function_exists) { $function($error_element, $error); @@ -325,8 +335,9 @@ // Make the changes we want to the form state. $field_name = $form_state['clicked_button']['#field_name']; + $language = $form_state['clicked_button']['#language']; if ($form_state['values'][$field_name . '_add_more']) { - $form_state['field_item_count'][$field_name] = count($form_state['values'][$field_name]); + $form_state['field_item_count'][$field_name] = count($form_state['values'][$field_name][$language]); } } } @@ -358,7 +369,7 @@ $instance = $form['#fields'][$field_name]['instance']; $form_path = $form['#fields'][$field_name]['form_path']; if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED) { - // Ivnalid + // Invalid $invalid = TRUE; } @@ -388,19 +399,25 @@ // Reset cached ids, so that they don't affect the actual form we output. drupal_static_reset('form_clean_id'); + // Ensure that a valid language is provided. + $language = key($_POST[$field_name]); + if ($language != FIELD_LANGUAGE_NONE) { + $language = field_multilingual_valid_language($language); + } + // Sort the $form_state['values'] we just built *and* the incoming $_POST data // according to d-n-d reordering. - unset($form_state['values'][$field_name][$field['field_name'] . '_add_more']); - foreach ($_POST[$field_name] as $delta => $item) { - $form_state['values'][$field_name][$delta]['_weight'] = $item['_weight']; + unset($form_state['values'][$field_name][$language][$field['field_name'] . '_add_more']); + foreach ($_POST[$field_name][$language] as $delta => $item) { + $form_state['values'][$field_name][$language][$delta]['_weight'] = $item['_weight']; } - $form_state['values'][$field_name] = _field_sort_items($field, $form_state['values'][$field_name]); - $_POST[$field_name] = _field_sort_items($field, $_POST[$field_name]); + $form_state['values'][$field_name][$language] = _field_sort_items($field, $form_state['values'][$field_name][$language]); + $_POST[$field_name][$language] = _field_sort_items($field, $_POST[$field_name][$language]); // Build our new form element for the whole field, asking for one more element. - $form_state['field_item_count'] = array($field_name => count($_POST[$field_name]) + 1); - $items = $form_state['values'][$field_name]; - $form_element = field_default_form(NULL, NULL, $field, $instance, $items, $form, $form_state); + $form_state['field_item_count'] = array($field_name => count($_POST[$field_name][$language]) + 1); + $items = $form_state['values'][$field_name][$language]; + $form_element = field_default_form(NULL, NULL, $field, $instance, $language, $items, $form, $form_state); // Let other modules alter it. drupal_alter('form', $form_element, array(), 'field_add_more_js'); @@ -417,8 +434,8 @@ // Build the new form against the incoming $_POST values so that we can // render the new element. - $delta = max(array_keys($_POST[$field_name])) + 1; - $_POST[$field_name][$delta]['_weight'] = $delta; + $delta = max(array_keys($_POST[$field_name][$language])) + 1; + $_POST[$field_name][$language][$delta]['_weight'] = $delta; $form_state = form_state_defaults(); $form_state['input'] = $_POST; $form = form_builder($_POST['form_id'], $form, $form_state); Index: modules/field/field.info.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.info.inc,v retrieving revision 1.11 diff -u -r1.11 field.info.inc --- modules/field/field.info.inc 2 Aug 2009 11:24:21 -0000 1.11 +++ modules/field/field.info.inc 3 Aug 2009 18:18:16 -0000 @@ -117,6 +117,7 @@ // Provide defaults. $fieldable_info += array( 'cacheable' => TRUE, + 'translation_handlers' => array(), 'bundles' => array(), ); $fieldable_info['object keys'] += array( Index: modules/field/field.install =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.install,v retrieving revision 1.9 diff -u -r1.9 field.install --- modules/field/field.install 28 May 2009 10:05:32 -0000 1.9 +++ modules/field/field.install 3 Aug 2009 18:18:16 -0000 @@ -63,6 +63,12 @@ 'not null' => TRUE, 'default' => 0, ), + 'translatable' => array( + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), 'active' => array( 'type' => 'int', 'size' => 'tiny', Index: modules/field/modules/number/number.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/number/number.module,v retrieving revision 1.11 diff -u -r1.11 number.module --- modules/field/modules/number/number.module 1 Aug 2009 06:03:12 -0000 1.11 +++ modules/field/modules/number/number.module 3 Aug 2009 18:18:18 -0000 @@ -94,17 +94,17 @@ * - 'number_min': The value is smaller than the allowed minimum value. * - 'number_max': The value is larger than the allowed maximum value. */ -function number_field_validate($obj_type, $node, $field, $instance, $items, &$errors) { +function number_field_validate($obj_type, $node, $field, $instance, $language, $items, &$errors) { foreach ($items as $delta => $item) { if ($item['value'] != '') { if (is_numeric($instance['settings']['min']) && $item['value'] < $instance['settings']['min']) { - $errors[$field['field_name']][$delta][] = array( + $errors[$field['field_name']][$language][$delta][] = array( 'error' => 'number_min', 'message' => t('%name: the value may be no smaller than %min.', array('%name' => t($instance['label']), '%min' => $instance['settings']['min'])), ); } if (is_numeric($instance['settings']['max']) && $item['value'] > $instance['settings']['max']) { - $errors[$field['field_name']][$delta][] = array( + $errors[$field['field_name']][$language][$delta][] = array( 'error' => 'number_max', 'message' => t('%name: the value may be no larger than %max.', array('%name' => t($instance['label']), '%max' => $instance['settings']['max'])), ); Index: modules/taxonomy/taxonomy.test =================================================================== RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.test,v retrieving revision 1.41 diff -u -r1.41 taxonomy.test --- modules/taxonomy/taxonomy.test 31 Jul 2009 07:43:33 -0000 1.41 +++ modules/taxonomy/taxonomy.test 3 Aug 2009 18:18:27 -0000 @@ -477,7 +477,7 @@ // Post an article. $edit = array(); $edit['title'] = $this->randomName(); - $edit['body[0][value]'] = $this->randomName(); + $edit['body[' . FIELD_LANGUAGE_NONE . '][0][value]'] = $this->randomName(); $edit['taxonomy[' . $this->vocabulary->vid . ']'] = $term1->tid; $this->drupalPost('node/add/article', $edit, t('Save')); @@ -519,7 +519,7 @@ // Insert the terms in a comma separated list. Vocabulary 1 is a // free-tagging field created by the default profile. $edit['taxonomy[tags][' . $this->vocabulary->vid . ']'] = implode(', ', $terms); - $edit['body[0][value]'] = $this->randomName(); + $edit['body[' . FIELD_LANGUAGE_NONE . '][0][value]'] = $this->randomName(); $this->drupalPost('node/add/article', $edit, t('Save')); $this->assertRaw(t('@type %title has been created.', array('@type' => t('Article'), '%title' => $edit['title'])), t('The node was created successfully')); foreach ($terms as $term) { Index: modules/simpletest/tests/field_test.module =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/field_test.module,v retrieving revision 1.13 diff -u -r1.13 field_test.module --- modules/simpletest/tests/field_test.module 10 Jul 2009 05:58:13 -0000 1.13 +++ modules/simpletest/tests/field_test.module 3 Aug 2009 18:18:26 -0000 @@ -88,6 +88,15 @@ } /** + * Implement hook_fieldable_info_alter(). + */ +function field_test_fieldable_info_alter(&$info) { + foreach (field_test_fieldable_info_translatable() as $obj_type => $translatable) { + $info[$obj_type]['translation_handlers']['field_test'] = TRUE; + } +} + +/** * Create a new bundle for test_entity objects. * * @param $bundle_name @@ -372,7 +381,7 @@ /** * Implement hook_field_load(). */ -function field_test_field_load($obj_type, $objects, $field, $instances, &$items, $age) { +function field_test_field_load($obj_type, $objects, $field, $instances, $language, &$items, $age) { foreach ($items as $id => $item) { // To keep the test non-intrusive, only act for instances with the // test_hook_field_load setting explicitly set to TRUE. @@ -393,10 +402,10 @@ * Possible error codes: * - 'field_test_invalid': The value is invalid. */ -function field_test_field_validate($obj_type, $object, $field, $instance, $items, &$errors) { +function field_test_field_validate($obj_type, $object, $field, $instance, $language, $items, &$errors) { foreach ($items as $delta => $item) { if ($item['value'] == -1) { - $errors[$field['field_name']][$delta][] = array( + $errors[$field['field_name']][$language][$delta][] = array( 'error' => 'field_test_invalid', 'message' => t('%name does not accept the value -1.', array('%name' => $instance['label'])), ); @@ -407,7 +416,7 @@ /** * Implement hook_field_sanitize(). */ -function field_test_field_sanitize($obj_type, $object, $field, $instance, &$items) { +function field_test_field_sanitize($obj_type, $object, $field, $instance, $language, &$items) { foreach ($items as $delta => $item) { $value = check_plain($item['value']); $items[$delta]['safe'] = $value; @@ -478,8 +487,8 @@ * holds the field's form values. * @param $field * The field structure. - * @param $insatnce - * the insatnce array + * @param $instance + * the instance array * @param $items * array of default values for this field * @param $delta @@ -581,6 +590,48 @@ } /** + * Generic op to test _field_invoke behavior. + */ +function field_test_field_test_op($obj_type, $object, $field, $instance, $language, &$items) { + return array($language => md5(serialize(array($obj_type, $object, $field['field_name'], $language, $items)))); +} + +/** + * Generic op to test _field_invoke_multiple behavior. + */ +function field_test_field_test_op_multiple($obj_type, $objects, $field, $instances, $language, &$items) { + $result = array(); + foreach ($objects as $id => $object) { + $result[$id] = array($language => md5(serialize(array($obj_type, $object, $field['field_name'], $language, $items[$id])))); + } + return $result; +} + +/** + * Implement hook_field_languages(). + */ +function field_test_field_languages($obj_type, $field, &$languages) { + if ($field['settings']['test_hook_in']) { + // Add an unavailable language. + $languages[] = 'xx'; + // Remove an available language. + unset($languages[0]); + } +} + +/** + * Helper function to enable entity translations. + */ +function field_test_fieldable_info_translatable($obj_type = NULL, $translatable = NULL) { + $stored_value = &drupal_static(__FUNCTION__, array()); + if (isset($obj_type) && isset($translatable)) { + $stored_value[$obj_type] = $translatable; + _field_info_collate_types(TRUE); + } + return $stored_value; +} + +/** * Store and retrieve keyed data for later verification by unit tests. * * This function is a simple in-memory key-value store with the Index: modules/simpletest/tests/common.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/common.test,v retrieving revision 1.57 diff -u -r1.57 common.test --- modules/simpletest/tests/common.test 30 Jul 2009 19:57:10 -0000 1.57 +++ modules/simpletest/tests/common.test 3 Aug 2009 18:18:26 -0000 @@ -271,7 +271,7 @@ // Create a node, using the PHP filter that tests drupal_add_css(). $settings = array( 'type' => 'page', - 'body' => array(array('value' => t('This tests the inline CSS!') . "", 'format' => 3)), // PHP filter. + 'body' => array(FIELD_LANGUAGE_NONE => array(array('value' => t('This tests the inline CSS!') . "", 'format' => 3))), // PHP filter. 'promote' => 1, ); $node = $this->drupalCreateNode($settings); Index: modules/field/modules/field_sql_storage/field_sql_storage.test =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/field_sql_storage/field_sql_storage.test,v retrieving revision 1.6 diff -u -r1.6 field_sql_storage.test --- modules/field/modules/field_sql_storage/field_sql_storage.test 28 Jul 2009 19:18:06 -0000 1.6 +++ modules/field/modules/field_sql_storage/field_sql_storage.test 3 Aug 2009 18:18:18 -0000 @@ -56,9 +56,10 @@ function testFieldAttachLoad() { $entity_type = 'test_entity'; $eid = 0; + $language = FIELD_LANGUAGE_NONE; $etid = _field_sql_storage_etid($entity_type); - $columns = array('etid', 'entity_id', 'revision_id', 'delta', $this->field_name . '_value'); + $columns = array('etid', 'entity_id', 'revision_id', 'delta', 'language', $this->field_name . '_value'); // Insert data for four revisions to the field revisions table $query = db_insert($this->revision_table)->fields($columns); @@ -68,7 +69,7 @@ for ($delta = 0; $delta <= $this->field['cardinality']; $delta++) { $value = mt_rand(1, 127); $values[$evid][] = $value; - $query->values(array($etid, $eid, $evid, $delta, $value)); + $query->values(array($etid, $eid, $evid, $delta, $language, $value)); } } $query->execute(); @@ -76,7 +77,7 @@ // Insert data for the "most current revision" into the field table $query = db_insert($this->table)->fields($columns); foreach ($values[0] as $delta => $value) { - $query->values(array($etid, $eid, 0, $delta, $value)); + $query->values(array($etid, $eid, 0, $delta, $language, $value)); } $query->execute(); @@ -85,10 +86,10 @@ field_attach_load($entity_type, array($eid => $entity)); foreach ($values[0] as $delta => $value) { if ($delta < $this->field['cardinality']) { - $this->assertEqual($entity->{$this->field_name}[$delta]['value'], $value, "Value $delta is loaded correctly for current revision"); + $this->assertEqual($entity->{$this->field_name}[$language][$delta]['value'], $value, "Value $delta is loaded correctly for current revision"); } else { - $this->assertFalse(array_key_exists($delta, $entity->{$this->field_name}), "No extraneous value gets loaded for current revision."); + $this->assertFalse(array_key_exists($delta, $entity->{$this->field_name}[$language]), "No extraneous value gets loaded for current revision."); } } @@ -98,13 +99,23 @@ field_attach_load_revision($entity_type, array($eid => $entity)); foreach ($values[$evid] as $delta => $value) { if ($delta < $this->field['cardinality']) { - $this->assertEqual($entity->{$this->field_name}[$delta]['value'], $value, "Value $delta for revision $evid is loaded correctly"); + $this->assertEqual($entity->{$this->field_name}[$language][$delta]['value'], $value, "Value $delta for revision $evid is loaded correctly"); } else { - $this->assertFalse(array_key_exists($delta, $entity->{$this->field_name}), "No extraneous value gets loaded for revision $evid."); + $this->assertFalse(array_key_exists($delta, $entity->{$this->field_name}[$language]), "No extraneous value gets loaded for revision $evid."); } } } + + // Add a translation in an unavailable language and verify it is not loaded. + $eid = $evid = 1; + $unavailable_language = 'xx'; + $entity = field_test_create_stub_entity($eid, $evid, $this->instance['bundle']); + $values = array($etid, $eid, $evid, 0, $unavailable_language, mt_rand(1, 127)); + db_insert($this->table)->fields($columns)->values($values)->execute(); + db_insert($this->revision_table)->fields($columns)->values($values)->execute(); + field_attach_load($entity_type, array($eid => $entity)); + $this->assertFalse(array_key_exists($unavailable_language, $entity->{$this->field_name}), 'Field translation in an unavailable language ignored'); } /** @@ -114,6 +125,7 @@ function testFieldAttachInsertAndUpdate() { $entity_type = 'test_entity'; $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $language = FIELD_LANGUAGE_NONE; // Test insert. $values = array(); @@ -122,7 +134,7 @@ for ($delta = 0; $delta <= $this->field['cardinality']; $delta++) { $values[$delta]['value'] = mt_rand(1, 127); } - $entity->{$this->field_name} = $rev_values[0] = $values; + $entity->{$this->field_name}[$language] = $rev_values[0] = $values; field_attach_insert($entity_type, $entity); $rows = db_select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', PDO::FETCH_ASSOC); @@ -142,7 +154,7 @@ for ($delta = 0; $delta <= $this->field['cardinality']; $delta++) { $values[$delta]['value'] = mt_rand(1, 127); } - $entity->{$this->field_name} = $rev_values[1] = $values; + $entity->{$this->field_name}[$language] = $rev_values[1] = $values; field_attach_update($entity_type, $entity); $rows = db_select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', PDO::FETCH_ASSOC); foreach ($values as $delta => $value) { @@ -170,9 +182,9 @@ } $this->assertTrue(empty($rev_values), "All values for all revisions are stored in revision table {$this->revision_table}"); - // Check that update leaves the field data untouched if $object has no - // $field_name key. - unset($entity->{$this->field_name}); + // Check that update leaves the field data untouched if $object->{$field_name} has no + // language key. + unset($entity->{$this->field_name}[$language]); field_attach_update($entity_type, $entity); $rows = db_select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', PDO::FETCH_ASSOC); foreach ($values as $delta => $value) { @@ -182,7 +194,7 @@ } // Check that update with an empty $object->$field_name empties the field. - $entity->{$this->field_name} = NULL; + $entity->{$this->field_name}[$language] = NULL; field_attach_update($entity_type, $entity); $rows = db_select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', PDO::FETCH_ASSOC); $this->assertEqual(count($rows), 0, t("Update with an empty field_name entry empties the field.")); @@ -194,6 +206,7 @@ function testFieldAttachSaveMissingData() { $entity_type = 'test_entity'; $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $language = FIELD_LANGUAGE_NONE; // Insert: Field is missing field_attach_insert($entity_type, $entity); @@ -204,7 +217,7 @@ $this->assertEqual($count, 0, 'Missing field results in no inserts'); // Insert: Field is NULL - $entity->{$this->field_name} = NULL; + $entity->{$this->field_name}[$language] = NULL; field_attach_insert($entity_type, $entity); $count = db_select($this->table) ->countQuery() @@ -213,7 +226,7 @@ $this->assertEqual($count, 0, 'NULL field results in no inserts'); // Add some real data - $entity->{$this->field_name} = array(0 => array('value' => 1)); + $entity->{$this->field_name}[$language] = array(0 => array('value' => 1)); field_attach_insert($entity_type, $entity); $count = db_select($this->table) ->countQuery() @@ -238,5 +251,45 @@ ->execute() ->fetchField(); $this->assertEqual($count, 0, 'NULL field leaves no data in table'); + + // Add a translation in an unavailable language. + $unavailable_language = 'xx'; + db_insert($this->table) + ->fields(array('etid', 'bundle', 'deleted', 'entity_id', 'revision_id', 'delta', 'language')) + ->values(array(_field_sql_storage_etid($entity_type), $this->instance['bundle'], 0, 0, 0, 0, $unavailable_language)) + ->execute(); + $count = db_select($this->table) + ->countQuery() + ->execute() + ->fetchField(); + $this->assertEqual($count, 1, 'Field translation in an unavailable language saved'); + + // Again add some real data. + $entity->{$this->field_name}[$language] = array(0 => array('value' => 1)); + field_attach_insert($entity_type, $entity); + $count = db_select($this->table) + ->countQuery() + ->execute() + ->fetchField(); + $this->assertEqual($count, 2, 'Field data saved'); + + // Update: Field translation is missing but Field is not empty. Translation data should survive. + $entity->{$this->field_name}[$unavailable_language] = array(mt_rand(1, 127)); + unset($entity->{$this->field_name}[$language]); + field_attach_update($entity_type, $entity); + $count = db_select($this->table) + ->countQuery() + ->execute() + ->fetchField(); + $this->assertEqual($count, 2, 'Missing field translation leaves data in table'); + + // Update: Field translation is NULL but Field is not empty. Translation data should be wiped. + $entity->{$this->field_name}[$language] = NULL; + field_attach_update($entity_type, $entity); + $count = db_select($this->table) + ->countQuery() + ->execute() + ->fetchField(); + $this->assertEqual($count, 1, 'NULL field translation is wiped'); } } Index: modules/field/modules/field_sql_storage/field_sql_storage.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/field_sql_storage/field_sql_storage.module,v retrieving revision 1.17 diff -u -r1.17 field_sql_storage.module --- modules/field/modules/field_sql_storage/field_sql_storage.module 15 Jul 2009 17:55:18 -0000 1.17 +++ modules/field/modules/field_sql_storage/field_sql_storage.module 3 Aug 2009 18:18:18 -0000 @@ -144,8 +144,16 @@ 'not null' => TRUE, 'description' => 'The sequence number for this data item, used for multi-value fields', ), + // TODO: Consider an integer field for 'language'. + 'language' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The language for this data item.', + ), ), - 'primary key' => array('etid', 'entity_id', 'deleted', 'delta'), + 'primary key' => array('etid', 'entity_id', 'deleted', 'delta', 'language'), // TODO : index on 'bundle' ); @@ -169,7 +177,7 @@ $revision = $current; $revision['description'] = "Revision archive storage for {$deleted}field {$field['id']} ({$field['field_name']})"; $revision['revision_id']['description'] = 'The entity revision id this data is attached to'; - $revision['primary key'] = array('etid', 'revision_id', 'deleted', 'delta'); + $revision['primary key'] = array('etid', 'revision_id', 'deleted', 'delta', 'language'); return array( _field_sql_storage_tablename($field) => $current, @@ -215,7 +223,7 @@ if (!isset($skip_fields[$field_name]) && (!isset($options['field_name']) || $options['field_name'] == $instance['field_name'])) { $objects[$id]->{$field_name} = array(); $field_ids[$field_name][] = $load_current ? $id : $vid; - $delta_count[$id][$field_name] = 0; + $delta_count[$id][$field_name] = array(); } } } @@ -229,12 +237,17 @@ ->condition('etid', $etid) ->condition($load_current ? 'entity_id' : 'revision_id', $ids, 'IN') ->condition('deleted', 0) + ->condition('language', field_multilingual_available_languages($obj_type, $field), 'IN') ->orderBy('delta'); $results = $query->execute(); foreach ($results as $row) { - if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta_count[$row->entity_id][$field_name] < $field['cardinality']) { + if (!isset($delta_count[$row->entity_id][$field_name][$row->language])) { + $delta_count[$row->entity_id][$field_name][$row->language] = 0; + } + + if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta_count[$row->entity_id][$field_name][$row->language] < $field['cardinality']) { $item = array(); // For each column declared by the field, populate the item // from the prefixed database column. @@ -244,8 +257,8 @@ } // Add the item to the field values for the entity. - $objects[$row->entity_id]->{$field_name}[] = $item; - $delta_count[$row->entity_id][$field_name]++; + $objects[$row->entity_id]->{$field_name}[$row->language][] = $item; + $delta_count[$row->entity_id][$field_name][$row->language]++; } } } @@ -275,17 +288,33 @@ // Function property_exists() is slower, so we catch the more frequent cases // where it's an empty array with the faster isset(). if (isset($object->$field_name) || property_exists($object, $field_name)) { + $available_languages = field_multilingual_available_languages($obj_type, $field); + $available_translations = is_array($object->$field_name) ? array_intersect($available_languages, array_keys($object->$field_name)) : FALSE; + // Delete and insert, rather than update, in case a value was added. - if ($op == FIELD_STORAGE_UPDATE) { - db_delete($table_name)->condition('etid', $etid)->condition('entity_id', $id)->execute(); + // If no translation is available, empty the field for all the available languages. + if ($op == FIELD_STORAGE_UPDATE && count($available_translations)) { + $languages = empty($object->$field_name) ? $available_languages : $available_translations; + + db_delete($table_name) + ->condition('etid', $etid) + ->condition('entity_id', $id) + ->condition('language', $languages, 'IN') + ->execute(); + if (isset($vid)) { - db_delete($revision_name)->condition('etid', $etid)->condition('entity_id', $id)->condition('revision_id', $vid)->execute(); + db_delete($revision_name) + ->condition('etid', $etid) + ->condition('entity_id', $id) + ->condition('revision_id', $vid) + ->condition('language', $languages, 'IN') + ->execute(); } } - if ($object->$field_name) { + if (!empty($available_translations)) { // Prepare the multi-insert query. - $columns = array('etid', 'entity_id', 'revision_id', 'bundle', 'delta'); + $columns = array('etid', 'entity_id', 'revision_id', 'bundle', 'delta', 'language'); foreach ($field['columns'] as $column => $attributes) { $columns[] = _field_sql_storage_columnname($field_name, $column); } @@ -294,25 +323,30 @@ $revision_query = db_insert($revision_name)->fields($columns); } - $delta_count = 0; - foreach ($object->$field_name as $delta => $item) { - $record = array( - 'etid' => $etid, - 'entity_id' => $id, - 'revision_id' => $vid, - 'bundle' => $bundle, - 'delta' => $delta, - ); - foreach ($field['columns'] as $column => $attributes) { - $record[_field_sql_storage_columnname($field_name, $column)] = isset($item[$column]) ? $item[$column] : NULL; - } - $query->values($record); - if (isset($vid)) { - $revision_query->values($record); - } - - if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED && ++$delta_count == $field['cardinality']) { - break; + foreach ($available_translations as $language) { + if ($items = $object->{$field_name}[$language]) { + $delta_count = 0; + foreach ($items as $delta => $item) { + $record = array( + 'etid' => $etid, + 'entity_id' => $id, + 'revision_id' => $vid, + 'bundle' => $bundle, + 'delta' => $delta, + 'language' => $language, + ); + foreach ($field['columns'] as $column => $attributes) { + $record[_field_sql_storage_columnname($field_name, $column)] = isset($item[$column]) ? $item[$column] : NULL; + } + $query->values($record); + if (isset($vid)) { + $revision_query->values($record); + } + + if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED && ++$delta_count == $field['cardinality']) { + break; + } + } } } Index: modules/translation/translation.test =================================================================== RCS file: /cvs/drupal/drupal/modules/translation/translation.test,v retrieving revision 1.14 diff -u -r1.14 translation.test --- modules/translation/translation.test 20 Jul 2009 18:51:34 -0000 1.14 +++ modules/translation/translation.test 3 Aug 2009 18:18:27 -0000 @@ -60,14 +60,14 @@ // to return to the page then resubmitting the form without a refresh. $edit = array(); $edit['title'] = $this->randomName(); - $edit['body[0][value]'] = $this->randomName(); + $edit['body[' . FIELD_LANGUAGE_NONE . '][0][value]'] = $this->randomName(); $this->drupalPost('node/add/page', $edit, t('Save'), array('query' => array('translation' => $node->nid, 'language' => 'es'))); $duplicate = $this->drupalGetNodeByTitle($edit['title']); $this->assertEqual($duplicate->tnid, 0, t('The node does not have a tnid.')); // Update original and mark translation as outdated. $edit = array(); - $edit['body[0][value]'] = $this->randomName(); + $edit['body[' . FIELD_LANGUAGE_NONE . '][0][value]'] = $this->randomName(); $edit['translation[retranslate]'] = TRUE; $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); $this->assertRaw(t('Page %title has been updated.', array('%title' => $node_title)), t('Original node updated.')); @@ -78,7 +78,7 @@ // Update translation and mark as updated. $edit = array(); - $edit['body[0][value]'] = $this->randomName(); + $edit['body[' . FIELD_LANGUAGE_NONE . '][0][value]'] = $this->randomName(); $edit['translation[status]'] = FALSE; $this->drupalPost('node/' . $node_translation->nid . '/edit', $edit, t('Save')); $this->assertRaw(t('Page %title has been updated.', array('%title' => $node_translation_title)), t('Translated node updated.')); @@ -128,7 +128,7 @@ function createPage($title, $body, $language) { $edit = array(); $edit['title'] = $title; - $edit['body[0][value]'] = $body; + $edit['body[' . FIELD_LANGUAGE_NONE . '][0][value]'] = $body; $edit['language'] = $language; $this->drupalPost('node/add/page', $edit, t('Save')); $this->assertRaw(t('Page %title has been created.', array('%title' => $edit['title'])), t('Page created.')); @@ -153,7 +153,7 @@ $edit = array(); $edit['title'] = $title; - $edit['body[0][value]'] = $body; + $edit['body[' . FIELD_LANGUAGE_NONE . '][0][value]'] = $body; $this->drupalPost(NULL, $edit, t('Save')); $this->assertRaw(t('Page %title has been created.', array('%title' => $edit['title'])), t('Translation created.')); Index: modules/blogapi/blogapi.module =================================================================== RCS file: /cvs/drupal/drupal/modules/blogapi/blogapi.module,v retrieving revision 1.158 diff -u -r1.158 blogapi.module --- modules/blogapi/blogapi.module 5 Jul 2009 18:00:07 -0000 1.158 +++ modules/blogapi/blogapi.module 3 Aug 2009 18:18:14 -0000 @@ -213,7 +213,7 @@ } else { $edit['title'] = blogapi_blogger_title($content); - $edit['body'][0]['value'] = $content; + $edit['body'][FIELD_LANGUAGE_NONE][0]['value'] = $content; } if (!node_access('create', $edit['type'])) { @@ -274,12 +274,12 @@ // Check for bloggerAPI vs. metaWeblogAPI. if (is_array($content)) { $node->title = $content['title']; - $node->body[0]['value'] = $content['description']; + $node->body[FIELD_LANGUAGE_NONE][0]['value'] = $content['description']; _blogapi_mt_extra($node, $content); } else { $node->title = blogapi_blogger_title($content); - $node->body[0]['value'] = $content; + $node->body[FIELD_LANGUAGE_NONE][0]['value'] = $content; } module_invoke_all('node_blogapi_edit', $node); @@ -891,14 +891,14 @@ // Merge the 3 body sections (description, mt_excerpt, mt_text_more) into one body. if ($struct['mt_excerpt']) { - $node->body[0]['value'] = $struct['mt_excerpt'] . '' . $node->body[0]['value']; + $node->body[FIELD_LANGUAGE_NONE][0]['value'] = $struct['mt_excerpt'] . '' . $node->body[FIELD_LANGUAGE_NONE][0]['value']; } if ($struct['mt_text_more']) { - $node->body[0]['value'] = $node->body[0]['value'] . '' . $struct['mt_text_more']; + $node->body[FIELD_LANGUAGE_NONE][0]['value'] = $node->body[FIELD_LANGUAGE_NONE][0]['value'] . '' . $struct['mt_text_more']; } if ($struct['mt_convert_breaks']) { - $node->body[0]['format'] = $struct['mt_convert_breaks']; + $node->body[FIELD_LANGUAGE_NONE][0]['format'] = $struct['mt_convert_breaks']; } if ($struct['dateCreated']) { @@ -921,8 +921,8 @@ ); if ($bodies) { - $body = $node->body[0]['value']; - $format = $node->body[0]['format']; + $body = $node->body[FIELD_LANGUAGE_NONE][0]['value']; + $format = $node->body[FIELD_LANGUAGE_NONE][0]['format']; if ($node->comment == 1) { $comment = 2; } Index: modules/field/modules/list/list.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/list/list.module,v retrieving revision 1.7 diff -u -r1.7 list.module --- modules/field/modules/list/list.module 1 Aug 2009 06:03:12 -0000 1.7 +++ modules/field/modules/list/list.module 3 Aug 2009 18:18:18 -0000 @@ -103,12 +103,12 @@ * Possible error codes: * - 'list_illegal_value': The value is not part of the list of allowed values. */ -function list_field_validate($obj_type, $object, $field, $instance, $items, &$errors) { +function list_field_validate($obj_type, $object, $field, $instance, $language, $items, &$errors) { $allowed_values = list_allowed_values($field); foreach ($items as $delta => $item) { if (!empty($item['value'])) { if (count($allowed_values) && !array_key_exists($item['value'], $allowed_values)) { - $errors[$field['field_name']][$delta][] = array( + $errors[$field['field_name']][$language][$delta][] = array( 'error' => 'list_illegal_value', 'message' => t('%name: illegal value.', array('%name' => t($instance['label']))), ); Index: modules/field/theme/field.tpl.php =================================================================== RCS file: /cvs/drupal/drupal/modules/field/theme/field.tpl.php,v retrieving revision 1.3 diff -u -r1.3 field.tpl.php --- modules/field/theme/field.tpl.php 22 Jun 2009 09:10:04 -0000 1.3 +++ modules/field/theme/field.tpl.php 3 Aug 2009 18:18:19 -0000 @@ -18,6 +18,8 @@ * - $label: The item label. * - $label_display: Position of label display, inline, above, or hidden. * - $field_empty: Whether the field has any valid value. + * - $field_language: The field language. + * - $field_translatable: Whether the field is translatable or not. * * Each $item in $items contains: * - 'view' - the themed view for that item Index: modules/filter/filter.test =================================================================== RCS file: /cvs/drupal/drupal/modules/filter/filter.test,v retrieving revision 1.28 diff -u -r1.28 filter.test --- modules/filter/filter.test 27 Jul 2009 20:15:35 -0000 1.28 +++ modules/filter/filter.test 3 Aug 2009 18:18:20 -0000 @@ -110,8 +110,8 @@ $edit = array(); $edit['title'] = $this->randomName(); - $edit['body[0][value]'] = $body . '' . $extra_text . ''; - $edit['body[0][value_format]'] = $filtered; + $edit['body[' . FIELD_LANGUAGE_NONE . '][0][value]'] = $body . '' . $extra_text . ''; + $edit['body[' . FIELD_LANGUAGE_NONE . '][0][value_format]'] = $filtered; $this->drupalPost('node/add/page', $edit, t('Save')); $this->assertRaw(t('Page %title has been created.', array('%title' => $edit['title'])), t('Filtered node created.')); Index: modules/path/path.test =================================================================== RCS file: /cvs/drupal/drupal/modules/path/path.test,v retrieving revision 1.16 diff -u -r1.16 path.test --- modules/path/path.test 14 Jul 2009 10:22:17 -0000 1.16 +++ modules/path/path.test 3 Aug 2009 18:18:23 -0000 @@ -180,6 +180,7 @@ $edit['langcode'] = 'fr'; $this->drupalPost('admin/international/language/add', $edit, t('Add language')); + drupal_static_reset('language_list'); // Set language negotiation to "Path prefix with fallback". variable_set('language_negotiation', LANGUAGE_NEGOTIATION_PATH); @@ -212,7 +213,7 @@ $this->clickLink(t('add translation')); $edit = array(); $edit['title'] = $this->randomName(); - $edit['body[0][value]'] = $this->randomName(); + $edit['body[' . FIELD_LANGUAGE_NONE . '][0][value]'] = $this->randomName(); $edit['path'] = $this->randomName(); $this->drupalPost(NULL, $edit, t('Save')); Index: modules/system/system.test =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.test,v retrieving revision 1.61 diff -u -r1.61 system.test --- modules/system/system.test 31 Jul 2009 11:20:43 -0000 1.61 +++ modules/system/system.test 3 Aug 2009 18:18:27 -0000 @@ -489,7 +489,7 @@ $edit = array( 'title' => $this->randomName(10), - 'body' => array(array('value' => $this->randomName(100))), + 'body' => array(FIELD_LANGUAGE_NONE => array(array('value' => $this->randomName(100)))), ); $node = $this->drupalCreateNode($edit); @@ -566,7 +566,7 @@ $edit = array( 'title' => $this->randomName(10), - 'body' => array(array('value' => $this->randomName(100))), + 'body' => array(FIELD_LANGUAGE_NONE => array(array('value' => $this->randomName(100)))), ); $node = $this->drupalCreateNode($edit); @@ -692,7 +692,7 @@ // Generate node content. $edit = array( 'title' => '!SimpleTest! ' . $title . $this->randomName(20), - 'body[0][value]' => '!SimpleTest! test body' . $this->randomName(200), + 'body[' . FIELD_LANGUAGE_NONE . '][0][value]' => '!SimpleTest! test body' . $this->randomName(200), ); // Create the node with HTML in the title. $this->drupalPost('node/add/page', $edit, t('Save')); Index: modules/aggregator/aggregator.test =================================================================== RCS file: /cvs/drupal/drupal/modules/aggregator/aggregator.test,v retrieving revision 1.28 diff -u -r1.28 aggregator.test --- modules/aggregator/aggregator.test 30 Jul 2009 19:24:20 -0000 1.28 +++ modules/aggregator/aggregator.test 3 Aug 2009 18:18:13 -0000 @@ -252,7 +252,7 @@ for ($i = 0; $i < 5; $i++) { $edit = array(); $edit['title'] = $this->randomName(); - $edit['body[0][value]'] = $this->randomName(); + $edit['body[' . FIELD_LANGUAGE_NONE . '][0][value]'] = $this->randomName(); $this->drupalPost('node/add/article', $edit, t('Save')); } } Index: modules/node/node.test =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.test,v retrieving revision 1.38 diff -u -r1.38 node.test --- modules/node/node.test 28 Jul 2009 19:18:06 -0000 1.38 +++ modules/node/node.test 3 Aug 2009 18:18:23 -0000 @@ -141,7 +141,7 @@ // Confirm the correct revision text appears on "view revisions" page. $this->drupalGet("node/$node->nid/revisions/$node->vid/view"); - $this->assertText($node->body[0]['value'], t('Correct text displays for version.')); + $this->assertText($node->body[FIELD_LANGUAGE_NONE][0]['value'], t('Correct text displays for version.')); // Confirm the correct log message appears on "revisions overview" page. $this->drupalGet("node/$node->nid/revisions"); @@ -155,7 +155,7 @@ array('@type' => 'Page', '%title' => $nodes[1]->title, '%revision-date' => format_date($nodes[1]->revision_timestamp))), t('Revision reverted.')); $reverted_node = node_load($node->nid); - $this->assertTrue(($nodes[1]->body[0]['value'] == $reverted_node->body[0]['value']), t('Node reverted correctly.')); + $this->assertTrue(($nodes[1]->body[FIELD_LANGUAGE_NONE][0]['value'] == $reverted_node->body[FIELD_LANGUAGE_NONE][0]['value']), t('Node reverted correctly.')); // Confirm revisions delete properly. $this->drupalPost("node/$node->nid/revisions/{$nodes[1]->vid}/delete", array(), t('Delete')); @@ -186,7 +186,7 @@ * Check node edit functionality. */ function testPageEdit() { - $body_key = 'body[0][value]'; + $body_key = 'body[' . FIELD_LANGUAGE_NONE . '][0][value]'; // Create node to edit. $edit = array(); $edit['title'] = $this->randomName(8); @@ -241,7 +241,7 @@ * Check the node preview functionality. */ function testPagePreview() { - $body_key = 'body[0][value]'; + $body_key = 'body[' . FIELD_LANGUAGE_NONE . '][0][value]'; // Fill in node creation form and preview node. $edit = array(); @@ -263,7 +263,7 @@ * Check the node preview functionality, when using revisions. */ function testPagePreviewWithRevisions() { - $body_key = 'body[0][value]'; + $body_key = 'body[' . FIELD_LANGUAGE_NONE . '][0][value]'; // Force revision on page content. variable_set('node_options_page', array('status', 'revision')); @@ -311,7 +311,7 @@ // Create a node. $edit = array(); $edit['title'] = $this->randomName(8); - $edit['body[0][value]'] = $this->randomName(16); + $edit['body[' . FIELD_LANGUAGE_NONE . '][0][value]'] = $this->randomName(16); $this->drupalPost('node/add/page', $edit, t('Save')); // Check that the page has been created. @@ -377,7 +377,7 @@ function testSummaryLength() { // Create a node to view. $settings = array( - 'body' => array(array('value' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae arcu at leo cursus laoreet. Curabitur dui tortor, adipiscing malesuada tempor in, bibendum ac diam. Cras non tellus a libero pellentesque condimentum. What is a Drupalism? Suspendisse ac lacus libero. Ut non est vel nisl faucibus interdum nec sed leo. Pellentesque sem risus, vulputate eu semper eget, auctor in libero. Ut fermentum est vitae metus convallis scelerisque. Phasellus pellentesque rhoncus tellus, eu dignissim purus posuere id. Quisque eu fringilla ligula. Morbi ullamcorper, lorem et mattis egestas, tortor neque pretium velit, eget eleifend odio turpis eu purus. Donec vitae metus quis leo pretium tincidunt a pulvinar sem. Morbi adipiscing laoreet mauris vel placerat. Nullam elementum, nisl sit amet scelerisque malesuada, dolor nunc hendrerit quam, eu ultrices erat est in orci. Curabitur feugiat egestas nisl sed accumsan.')), + 'body' => array(FIELD_LANGUAGE_NONE => array(array('value' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae arcu at leo cursus laoreet. Curabitur dui tortor, adipiscing malesuada tempor in, bibendum ac diam. Cras non tellus a libero pellentesque condimentum. What is a Drupalism? Suspendisse ac lacus libero. Ut non est vel nisl faucibus interdum nec sed leo. Pellentesque sem risus, vulputate eu semper eget, auctor in libero. Ut fermentum est vitae metus convallis scelerisque. Phasellus pellentesque rhoncus tellus, eu dignissim purus posuere id. Quisque eu fringilla ligula. Morbi ullamcorper, lorem et mattis egestas, tortor neque pretium velit, eget eleifend odio turpis eu purus. Donec vitae metus quis leo pretium tincidunt a pulvinar sem. Morbi adipiscing laoreet mauris vel placerat. Nullam elementum, nisl sit amet scelerisque malesuada, dolor nunc hendrerit quam, eu ultrices erat est in orci. Curabitur feugiat egestas nisl sed accumsan.'))), 'promote' => 1, ); $node = $this->drupalCreateNode($settings); @@ -500,7 +500,7 @@ // Create a node. $edit = array(); $edit['title'] = $this->randomName(8); - $edit['body[0][value]'] = $this->randomName(16); + $edit['body[' . FIELD_LANGUAGE_NONE . '][0][value]'] = $this->randomName(16); $this->drupalPost('node/add/page', $edit, t('Save')); // Check that the post information is displayed. @@ -521,7 +521,7 @@ // Create a node. $edit = array(); $edit['title'] = $this->randomName(8); - $edit['body[0][value]'] = $this->randomName(16); + $edit['body[' . FIELD_LANGUAGE_NONE . '][0][value]'] = $this->randomName(16); $this->drupalPost('node/add/page', $edit, t('Save')); // Check that the post information is displayed. @@ -764,7 +764,7 @@ $title = $this->randomName(8); $node = array( 'title' => $title, - 'body' => array(array('value' => $this->randomName(32))), + 'body' => array(FIELD_LANGUAGE_NONE => array(array('value' => $this->randomName(32)))), 'uid' => $this->web_user->uid, 'type' => 'article', 'nid' => $test_nid, Index: modules/node/node.tpl.php =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.tpl.php,v retrieving revision 1.18 diff -u -r1.18 node.tpl.php --- modules/node/node.tpl.php 23 Jul 2009 22:12:53 -0000 1.18 +++ modules/node/node.tpl.php 3 Aug 2009 18:18:23 -0000 @@ -59,6 +59,13 @@ * - $is_front: Flags true when presented in the front page. * - $logged_in: Flags true when the current user is a logged-in member. * - $is_admin: Flags true when the current user is an administrator. + * + * Field variables: for each field instance attached to the node a corresponding + * variable is defined, e.g. $node->body becomes $body. When needing to access + * the field raw values developers/themers are strongly encouraged to use these + * variables, otherwise they will have to explicitly specify the desired field + * language, e.g. $node->body['en'], thus overriding any language negotiation + * rule previously applied. * * @see template_preprocess() * @see template_preprocess_node() Index: modules/node/node.pages.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.pages.inc,v retrieving revision 1.72 diff -u -r1.72 node.pages.inc --- modules/node/node.pages.inc 29 Jul 2009 06:39:34 -0000 1.72 +++ modules/node/node.pages.inc 3 Aug 2009 18:18:23 -0000 @@ -281,7 +281,7 @@ $form['#theme'] = array($node->type . '_node_form', 'node_form'); $form['#builder_function'] = 'node_form_submit_build_node'; - field_attach_form('node', $node, $form, $form_state); + field_attach_form('node', $node, $form, $form_state, $node->language); return $form; } Index: modules/node/node.module =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.module,v retrieving revision 1.1090 diff -u -r1.1090 node.module --- modules/node/node.module 31 Jul 2009 19:01:02 -0000 1.1090 +++ modules/node/node.module 3 Aug 2009 18:18:22 -0000 @@ -1139,7 +1139,11 @@ if (empty($node->content)) { $node->content = array(); }; - $node->content += field_attach_view('node', $node, $build_mode); + + // TODO: content language negotiation. + $content_language = $node->language; + drupal_alter('content_language', $content_language); + $node->content += field_attach_view('node', $node, $build_mode, $content_language); // Always display a read more link on teasers because we have no way // to know when a teaser view is different than a full view. @@ -1221,6 +1225,7 @@ // Flatten the node object's member fields. $variables = array_merge((array)$node, $variables); + field_attach_preprocess('node', $node, $node->content, $variables); // Display post information only on certain node types. if (variable_get('node_submitted_' . $node->type, TRUE)) { Index: modules/node/node.install =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.install,v retrieving revision 1.25 diff -u -r1.25 node.install --- modules/node/node.install 27 Jul 2009 19:26:31 -0000 1.25 +++ modules/node/node.install 3 Aug 2009 18:18:21 -0000 @@ -474,15 +474,15 @@ 'type' => $revision->type, ); if (!empty($revision->teaser) && $revision->teaser != text_summary($revision->body)) { - $node->body[0]['summary'] = $revision->teaser; + $node->body[FIELD_LANGUAGE_NONE][0]['summary'] = $revision->teaser; } // Do this after text_summary() above. $break = ''; if (substr($revision->body, 0, strlen($break)) == $break) { $revision->body = substr($revision->body, strlen($break)); } - $node->body[0]['value'] = $revision->body; - $node->body[0]['format'] = $revision->format; + $node->body[FIELD_LANGUAGE_NONE][0]['value'] = $revision->body; + $node->body[FIELD_LANGUAGE_NONE][0]['format'] = $revision->format; // This is a core update and no contrib modules are enabled yet, so // we can assume default field storage for a faster update. field_sql_storage_field_storage_write('node', $node, FIELD_STORAGE_INSERT, array()); Index: modules/book/book.test =================================================================== RCS file: /cvs/drupal/drupal/modules/book/book.test,v retrieving revision 1.13 diff -u -r1.13 book.test --- modules/book/book.test 20 Jul 2009 18:51:32 -0000 1.13 +++ modules/book/book.test 3 Aug 2009 18:18:14 -0000 @@ -141,7 +141,7 @@ // Check printer friendly version. $this->drupalGet('book/export/html/' . $node->nid); $this->assertText($node->title, t('Printer friendly title found.')); - $this->assertRaw(check_markup($node->body[0]['value'], $node->body[0]['format']), t('Printer friendly body found.')); + $this->assertRaw(check_markup($node->body[FIELD_LANGUAGE_NONE][0]['value'], $node->body[FIELD_LANGUAGE_NONE][0]['format']), t('Printer friendly body found.')); $number++; } @@ -173,7 +173,7 @@ $edit = array(); $edit['title'] = $number . ' - SimpleTest test node ' . $this->randomName(10); - $edit['body[0][value]'] = 'SimpleTest test body ' . $this->randomName(32) . ' ' . $this->randomName(32); + $edit['body[' . FIELD_LANGUAGE_NONE . '][0][value]'] = 'SimpleTest test body ' . $this->randomName(32) . ' ' . $this->randomName(32); $edit['book[bid]'] = $book_nid; if ($parent !== NULL) { Index: modules/blog/blog.test =================================================================== RCS file: /cvs/drupal/drupal/modules/blog/blog.test,v retrieving revision 1.17 diff -u -r1.17 blog.test --- modules/blog/blog.test 3 Aug 2009 03:04:33 -0000 1.17 +++ modules/blog/blog.test 3 Aug 2009 18:18:13 -0000 @@ -152,7 +152,7 @@ // Edit blog node. $edit = array(); $edit['title'] = 'node/' . $node->nid; - $edit['body[0][value]'] = $this->randomName(256); + $edit['body[' . FIELD_LANGUAGE_NONE . '][0][value]'] = $this->randomName(256); $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); $this->assertRaw(t('Blog entry %title has been updated.', array('%title' => $edit['title'])), t('Blog node was edited')); Index: modules/search/search.test =================================================================== RCS file: /cvs/drupal/drupal/modules/search/search.test,v retrieving revision 1.27 diff -u -r1.27 search.test --- modules/search/search.test 31 Jul 2009 19:01:02 -0000 1.27 +++ modules/search/search.test 3 Aug 2009 18:18:24 -0000 @@ -312,7 +312,7 @@ // Create nodes for testing. foreach ($node_ranks as $node_rank) { - $settings = array('type' => 'page', 'title' => 'Drupal rocks', 'body' => array(array('value' => "Drupal's search rocks"))); + $settings = array('type' => 'page', 'title' => 'Drupal rocks', 'body' => array(FIELD_LANGUAGE_NONE => array(array('value' => "Drupal's search rocks")))); foreach (array(0, 1) as $num) { if ($num == 1) { switch ($node_rank) { @@ -321,7 +321,7 @@ $settings[$node_rank] = 1; break; case 'relevance': - $settings['body'][0]['value'] .= " really rocks"; + $settings['body'][FIELD_LANGUAGE_NONE][0]['value'] .= " really rocks"; break; case 'recent': $settings['created'] = REQUEST_TIME + 3600; Index: modules/forum/forum.test =================================================================== RCS file: /cvs/drupal/drupal/modules/forum/forum.test,v retrieving revision 1.27 diff -u -r1.27 forum.test --- modules/forum/forum.test 3 Aug 2009 03:04:33 -0000 1.27 +++ modules/forum/forum.test 3 Aug 2009 18:18:20 -0000 @@ -234,7 +234,7 @@ $edit = array( 'title' => $title, - 'body[0][value]' => $body, + 'body[' . FIELD_LANGUAGE_NONE . '][0][value]' => $body, 'taxonomy[1]' => $tid ); @@ -321,7 +321,7 @@ // Edit forum node (including moving it to another forum). $edit = array(); $edit['title'] = 'node/' . $node->nid; - $edit['body[0][value]'] = $this->randomName(256); + $edit['body[' . FIELD_LANGUAGE_NONE . '][0][value]'] = $this->randomName(256); $edit['taxonomy[1]'] = $this->root_forum['tid']; // Assumes the topic is initially associated with $forum. $edit['shadow'] = TRUE; $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); Index: modules/user/user-profile.tpl.php =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user-profile.tpl.php,v retrieving revision 1.10 diff -u -r1.10 user-profile.tpl.php --- modules/user/user-profile.tpl.php 13 Jul 2009 21:09:54 -0000 1.10 +++ modules/user/user-profile.tpl.php 3 Aug 2009 18:18:27 -0000 @@ -15,6 +15,13 @@ * is provided which contains data on the user's history. Other data can be * included by modules. $user_profile['user_picture'] is available * for showing the account picture. + * + * Field variables: for each field instance attached to the user a corresponding + * variable is defined, e.g. $user->field_example becomes $field_example. When + * needing to access the field raw values developers/themers are strongly encouraged + * to use these variables, otherwise they will have to explicitly specify the desired + * field language, e.g. $user->field_example['en'], thus overriding any language + * negotiation rule previously applied. * * @see user-profile-category.tpl.php * Where the html is handled for the group. Index: modules/trigger/trigger.test =================================================================== RCS file: /cvs/drupal/drupal/modules/trigger/trigger.test,v retrieving revision 1.15 diff -u -r1.15 trigger.test --- modules/trigger/trigger.test 28 Jul 2009 19:18:08 -0000 1.15 +++ modules/trigger/trigger.test 3 Aug 2009 18:18:27 -0000 @@ -38,7 +38,7 @@ $this->drupalLogin($web_user); $edit = array(); $edit['title'] = '!SimpleTest test node! ' . $this->randomName(10); - $edit['body[0][value]'] = '!SimpleTest test body! ' . $this->randomName(32) . ' ' . $this->randomName(32); + $edit['body[' . FIELD_LANGUAGE_NONE . '][0][value]'] = '!SimpleTest test body! ' . $this->randomName(32) . ' ' . $this->randomName(32); $edit[$info['property']] = !$info['expected']; $this->drupalPost('node/add/page', $edit, t('Save')); // Make sure the text we want appears. Index: modules/dblog/dblog.test =================================================================== RCS file: /cvs/drupal/drupal/modules/dblog/dblog.test,v retrieving revision 1.24 diff -u -r1.24 dblog.test --- modules/dblog/dblog.test 31 Jul 2009 19:01:01 -0000 1.24 +++ modules/dblog/dblog.test 3 Aug 2009 18:18:14 -0000 @@ -327,7 +327,7 @@ default: $content = array( 'title' => $this->randomName(8), - 'body[0][value]' => $this->randomName(32), + 'body[' . FIELD_LANGUAGE_NONE . '][0][value]' => $this->randomName(32), ); break; } @@ -351,7 +351,7 @@ default: $content = array( - 'body[0][value]' => $this->randomName(32), + 'body[' . FIELD_LANGUAGE_NONE . '][0][value]' => $this->randomName(32), ); break; } Index: modules/field/modules/text/text.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/text/text.module,v retrieving revision 1.16 diff -u -r1.16 text.module --- modules/field/modules/text/text.module 1 Aug 2009 06:03:12 -0000 1.16 +++ modules/field/modules/text/text.module 3 Aug 2009 18:18:19 -0000 @@ -135,7 +135,7 @@ * - 'text_value_max_length': The value exceeds the maximum length. * - 'text_summary_max_length': The summary exceeds the maximum length. */ -function text_field_validate($obj_type, $object, $field, $instance, $items, &$errors) { +function text_field_validate($obj_type, $object, $field, $instance, $language, $items, &$errors) { foreach ($items as $delta => $item) { foreach (array('value' => t('full text'), 'summary' => t('summary')) as $column => $desc) { if (!empty($item[$column])) { @@ -148,7 +148,7 @@ $message = t('%name: the summary may not be longer than %max characters.', array('%name' => $instance['label'], '%max' => $field['settings']['max_length'])); break; } - $errors[$field['field_name']][$delta][] = array( + $errors[$field['field_name']][$language][$delta][] = array( 'error' => "text_{$column}_length", 'message' => $message, ); @@ -166,9 +166,7 @@ * separately. * @see text_field_sanitize(). */ -function text_field_load($obj_type, $objects, $field, $instances, &$items) { - global $language; - +function text_field_load($obj_type, $objects, $field, $instances, $language, &$items) { foreach ($objects as $id => $object) { foreach ($items[$id] as $delta => $item) { if (!empty($instances[$id]['settings']['text_processing'])) { @@ -176,10 +174,9 @@ // handled by text_field_sanitize(). $format = $item['format']; if (filter_format_allowcache($format)) { - $lang = isset($object->language) ? $object->language : $language->language; - $items[$id][$delta]['safe'] = isset($item['value']) ? check_markup($item['value'], $format, $lang, FALSE, FALSE) : ''; + $items[$id][$delta]['safe'] = isset($item['value']) ? check_markup($item['value'], $format, $language, FALSE, FALSE) : ''; if ($field['type'] == 'text_with_summary') { - $items[$id][$delta]['safe_summary'] = isset($item['summary']) ? check_markup($item['summary'], $format, $lang, FALSE, FALSE) : ''; + $items[$id][$delta]['safe_summary'] = isset($item['summary']) ? check_markup($item['summary'], $format, $language, FALSE, FALSE) : ''; } } } @@ -198,7 +195,7 @@ * * @see text_field_load() */ -function text_field_sanitize($obj_type, $object, $field, $instance, &$items) { +function text_field_sanitize($obj_type, $object, $field, $instance, $language, &$items) { global $language; foreach ($items as $delta => $item) { // Only sanitize items which were not already processed inside Index: modules/field/modules/text/text.test =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/text/text.test,v retrieving revision 1.9 diff -u -r1.9 text.test --- modules/field/modules/text/text.test 27 Jul 2009 20:15:35 -0000 1.9 +++ modules/field/modules/text/text.test 3 Aug 2009 18:18:19 -0000 @@ -50,8 +50,9 @@ field_create_instance($this->instance); // Test valid and invalid values with field_attach_validate(). $entity = field_test_create_stub_entity(0, 0, FIELD_TEST_BUNDLE); + $language = FIELD_LANGUAGE_NONE; for ($i = 0; $i <= $max_length + 2; $i++) { - $entity->{$this->field['field_name']}[0]['value'] = str_repeat('x', $i); + $entity->{$this->field['field_name']}[$language][0]['value'] = str_repeat('x', $i); try { field_attach_validate('test_entity', $entity); $this->assertTrue($i <= $max_length, "Length $i does not cause validation error when max_length is $max_length"); @@ -91,16 +92,17 @@ ) ); field_create_instance($this->instance); + $language = FIELD_LANGUAGE_NONE; // Display creation form. $this->drupalGet('test-entity/add/test-bundle'); - $this->assertFieldByName($this->field_name . '[0][value]', '', t('Widget is displayed')); - $this->assertNoFieldByName($this->field_name . '[0][format]', '1', t('Format selector is not displayed')); + $this->assertFieldByName($this->field_name . '[' . $language . '][0][value]', '', t('Widget is displayed')); + $this->assertNoFieldByName($this->field_name . '[' . $language . '][0][format]', '1', t('Format selector is not displayed')); // Submit with some value. $value = $this->randomName(); $edit = array( - $this->field_name . '[0][value]' => $value, + $this->field_name . '[' . $language . '][0][value]' => $value, ); $this->drupalPost(NULL, $edit, t('Save')); preg_match('|test-entity/(\d+)/edit|', $this->url, $match); @@ -143,18 +145,19 @@ ) ); field_create_instance($this->instance); + $language = FIELD_LANGUAGE_NONE; // Display creation form. // By default, the user only has access to 'Filtered HTML', and no format // selector is displayed $this->drupalGet('test-entity/add/test-bundle'); - $this->assertFieldByName($this->field_name . '[0][value]', '', t('Widget is displayed')); - $this->assertNoFieldByName($this->field_name . '[0][value_format]', '1', t('Format selector is not displayed')); + $this->assertFieldByName($this->field_name . '[' . $language . '][0][value]', '', t('Widget is displayed')); + $this->assertNoFieldByName($this->field_name . '[' . $language . '][0][value_format]', '1', t('Format selector is not displayed')); // Submit with data that should be filtered. $value = $this->randomName() . '
' . $this->randomName(); $edit = array( - $this->field_name . '[0][value]' => $value, + $this->field_name . '[' . $language . '][0][value]' => $value, ); $this->drupalPost(NULL, $edit, t('Save')); preg_match('|test-entity/(\d+)/edit|', $this->url, $match); @@ -174,12 +177,12 @@ // Display edition form. // We should now have a 'text format' selector. $this->drupalGet('test-entity/' . $id . '/edit'); - $this->assertFieldByName($this->field_name . '[0][value]', '', t('Widget is displayed')); - $this->assertFieldByName($this->field_name . '[0][value_format]', '1', t('Format selector is displayed')); + $this->assertFieldByName($this->field_name . '[' . $language . '][0][value]', '', t('Widget is displayed')); + $this->assertFieldByName($this->field_name . '[' . $language . '][0][value_format]', '1', t('Format selector is displayed')); // Edit and change the format to 'Full HTML'. $edit = array( - $this->field_name . '[0][value_format]' => 2, + $this->field_name . '[' . $language . '][0][value_format]' => 2, ); $this->drupalPost(NULL, $edit, t('Save')); $this->assertRaw(t('test_entity @id has been updated.', array('@id' => $id)), t('Entity was updated')); Index: modules/php/php.test =================================================================== RCS file: /cvs/drupal/drupal/modules/php/php.test,v retrieving revision 1.14 diff -u -r1.14 php.test --- modules/php/php.test 13 Jul 2009 21:51:11 -0000 1.14 +++ modules/php/php.test 3 Aug 2009 18:18:24 -0000 @@ -23,7 +23,7 @@ * @return stdObject Node object. */ function createNodeWithCode() { - return $this->drupalCreateNode(array('body' => array(array('value' => '')))); + return $this->drupalCreateNode(array('body' => array(FIELD_LANGUAGE_NONE => array(array('value' => ''))))); } } @@ -60,7 +60,7 @@ // Change filter to PHP filter and see that PHP code is evaluated. $edit = array(); - $edit['body[0][value_format]'] = 3; + $edit['body[' . FIELD_LANGUAGE_NONE . '][0][value_format]'] = 3; $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); $this->assertRaw(t('Page %title has been updated.', array('%title' => $node->title)), t('PHP code filter turned on.')); Index: modules/field/field.multilingual.inc =================================================================== RCS file: modules/field/field.multilingual.inc diff -N modules/field/field.multilingual.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/field/field.multilingual.inc 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,105 @@ + NULL)); +} + + +/** + * TODO + * @param $obj_type + * @param $handler + * @return unknown_type + */ +function field_multilingual_check_translation_handler($obj_type, $handler) { + $obj_info = field_info_fieldable_types($obj_type); + return isset($obj_info['translation_handlers'][$handler]); +} + +/** + * Helper function which ensures the the given language code is valid. + * + * Check if the given language is one of the enabled languages, otherwise + * depnding on the $default variable's value return the default language code + * or the current language one. + * + * @params $language + * The language code to be checked. + * @params $default + * Whether return the default language code or the current one. + * @returns + * A valid language code. + */ +function field_multilingual_valid_language($language, $default = TRUE) { + $enabled_languages = field_multilingual_content_languages(); + if (in_array($language, $enabled_languages)) { + return $language; + } + // TODO: language neutral language codes + if ($language === '') { + return FIELD_LANGUAGE_NONE; + } + global $language; + $valid_language = $default ? language_default('language') : $language->language; + if (in_array($valid_language, $enabled_languages)) { + return $valid_language; + } + // TODO: throw a more specific exception + throw new FieldException('No valid content language could be found'); +}