Index: modules/field/field.form.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.form.inc,v retrieving revision 1.8 diff -u -r1.8 field.form.inc --- modules/field/field.form.inc 3 May 2009 09:49:32 -0000 1.8 +++ modules/field/field.form.inc 10 May 2009 22:58:47 -0000 @@ -11,7 +11,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) { @@ -287,7 +287,7 @@ /** * 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])) { $function = $instance['widget']['module'] . '_field_widget_error'; Index: modules/field/field.attach.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.attach.inc,v retrieving revision 1.15 diff -u -r1.15 field.attach.inc --- modules/field/field.attach.inc 9 May 2009 19:02:11 -0000 1.15 +++ modules/field/field.attach.inc 10 May 2009 22:58:47 -0000 @@ -171,23 +171,32 @@ foreach ($instances as $instance) { $field_name = $instance['field_name']; $field = field_info_field($field_name); - $items = isset($object->$field_name) ? $object->$field_name : array(); + $field_translations = array(); + + // Initialize field translations according to the available languages. + foreach (_field_available_languages($field, $instance) as $language) { + $field_translations[$language] = isset($object->{$field_name}[$language]) ? $object->{$field_name}[$language] : array(); + } $function = $default ? 'field_default_' . $op : $field['module'] . '_field_' . $op; if (drupal_function_exists($function)) { - $result = $function($obj_type, $object, $field, $instance, $items, $a, $b); - if (is_array($result)) { - $return = array_merge($return, $result); - } - else if (isset($result)) { - $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); + // Store the result into per-language separate fields. + if (is_array($result)) { + $return[$language] = isset($return[$language]) ? array_merge($return[$language], $result) : $result; + } + else if (isset($result)) { + $return[$language][] = $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 $items back in the field values, 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; - } } return $return; @@ -201,6 +210,40 @@ } /** + * TODO + */ +function _field_available_languages($field, $instance) { + static $field_languages; + + $field_name = $field['field_name']; + if (!isset($field_languages[$field_name])) { + if ($field['translatable']) { + $languages = $available_languages = array_keys(language_list()); + // TODO: $instance['languages'] might be populated somehow to let users/modules + // decide which languages are available for the current instance. + // - Should this exist or the following hook invocation should be enough? + // - Should this be an instance or a field setting? + // $languages = isset($instance['languages']) ? $instance['languages'] : $available_languages; + // TODO: Do we need this hook invocation? + foreach (module_implements('field_languages') as $module) { + $function = $module . '_field_languages'; + $function($field, $instance, $languages); + } + + // TODO: Consider distinguishing between content language and UI language: + // probably they should not be tied in any way. + // Accept only available languages. + $field_languages[$field_name] = array_intersect($available_languages, $languages); + } + else { + $field_languages[$field_name] = array(FIELD_LANGUAGE_NEUTRAL); + } + } + + return $field_languages[$field_name]; +} + +/** * @} End of "defgroup field_attach" * * The rest of the functions in this file are not in a group, but @@ -225,7 +268,23 @@ */ function _field_attach_form($obj_type, $object, &$form, $form_state) { // TODO : something's not right here : do we alter the form or return a value ? - $form += (array) _field_invoke_default('form', $obj_type, $object, $form, $form_state); + $result = _field_invoke_default('form', $obj_type, $object, $form, $form_state); + + // TODO: is this the right way? + $languages = language_list(); + $form['field_translations'] = array( + '#type' => 'fieldset', + '#title' => t('Field translations'), + '#tree' => TRUE + ); + foreach ($result as $language => $field_form) { + $form['field_translations'][$language] = array( + '#type' => 'fieldset', + '#title' => $language === FIELD_LANGUAGE_NEUTRAL ? t('Language neutral') : $languages[$language]->native, + '#tree' => TRUE, + ); + $form['field_translations'][$language] += (array) $field_form; + } // Let other modules make changes to the form. foreach (module_implements('field_attach_form') as $module) { @@ -633,13 +692,12 @@ // Let field modules sanitize their data for output. _field_invoke('sanitize', $obj_type, $object); - $output = _field_invoke_default('view', $obj_type, $object, $teaser); + $output = array('field_translations' => _field_invoke_default('view', $obj_type, $object, $teaser)); // Let other modules make changes after rendering the view. drupal_alter('field_attach_view', $output, $obj_type, $object, $teaser); return $output; - } /** Index: modules/field/field.install =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.install,v retrieving revision 1.4 diff -u -r1.4 field.install --- modules/field/field.install 10 Mar 2009 09:45:31 -0000 1.4 +++ modules/field/field.install 10 May 2009 22:58:48 -0000 @@ -58,6 +58,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/field.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.module,v retrieving revision 1.7 diff -u -r1.7 field.module --- modules/field/field.module 26 Mar 2009 13:31:24 -0000 1.7 +++ modules/field/field.module 10 May 2009 22:58:48 -0000 @@ -57,6 +57,11 @@ /** * TODO */ +define('FIELD_LANGUAGE_NEUTRAL', '_0'); + +/** + * TODO + */ define('FIELD_BEHAVIOR_NONE', 0x0001); /** * TODO Index: modules/field/field.test =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.test,v retrieving revision 1.17 diff -u -r1.17 field.test --- modules/field/field.test 3 May 2009 09:49:32 -0000 1.17 +++ modules/field/field.test 10 May 2009 22:58:49 -0000 @@ -65,6 +65,7 @@ function testFieldAttachSaveLoad() { $entity_type = 'test_entity'; $values = array(); + $language = FIELD_LANGUAGE_NEUTRAL; // TODO : test empty values filtering and "compression" (store consecutive deltas). @@ -78,12 +79,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]); } } @@ -92,10 +93,10 @@ $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('Current 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('Current revision: expected value %delta was found.', array('%delta' => $delta))); } // Confirm each revision loads the correct data. @@ -103,10 +104,10 @@ $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))); } } } @@ -117,6 +118,7 @@ function testFieldAttachSaveMissingData() { $entity_type = 'test_entity'; $entity_init = field_test_create_stub_entity(); + $language = FIELD_LANGUAGE_NEUTRAL; // Insert: Field is missing. $entity = clone($entity_init); @@ -129,23 +131,23 @@ // 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 = array(0 => array('value' => mt_rand(1, 127))); - $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(); @@ -154,17 +156,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')); } /** @@ -177,15 +179,16 @@ $entity_type = 'test_entity'; $entity_init = field_test_create_stub_entity(); + $language = FIELD_LANGUAGE_NEUTRAL; // 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(); @@ -195,19 +198,20 @@ $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')); } function testFieldAttachViewAndPreprocess() { $entity_type = 'test_entity'; $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $language = FIELD_LANGUAGE_NEUTRAL; // Populate values to be displayed. $values = array(); for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { $values[$delta]['value'] = mt_rand(1, 127); } - $entity->{$this->field_name} = $values; + $entity->{$this->field_name}[$language] = $values; // Simple formatter, label displayed. $formatter_setting = $this->randomName(); @@ -225,15 +229,15 @@ $output = drupal_render($entity->content); $variables = field_attach_preprocess($entity_type, $entity); $variable = $this->instance['field_name'] . '_rendered'; - $this->assertTrue(isset($variables[$variable]), "Variable $variable is available in templates."); + $this->assertTrue(isset($variables[$language][$variable]), "Variable $variable is available in templates."); $this->content = $output; $this->assertRaw($this->instance['label'], "Label is displayed."); - $this->content = $variables[$variable]; + $this->content = $variables[$language][$variable]; $this->assertRaw($this->instance['label'], "Label is displayed (template variable)."); foreach ($values as $delta => $value) { $this->content = $output; $this->assertRaw("$formatter_setting|{$value['value']}", "Value $delta is displayed, formatter settings are applied."); - $this->content = $variables[$variable]; + $this->content = $variables[$language][$variable]; $this->assertRaw("$formatter_setting|{$value['value']}", "Value $delta is displayed, formatter settings are applied (template variable)."); } @@ -245,7 +249,7 @@ $variables = field_attach_preprocess($entity_type, $entity); $this->content = $output; $this->assertNoRaw($this->instance['label'], "Hidden label: label is not displayed."); - $this->content = $variables[$variable]; + $this->content = $variables[$language][$variable]; $this->assertNoRaw($this->instance['label'], "Hidden label: label is not displayed (template variable)."); // Field hidden. @@ -260,7 +264,7 @@ $entity->content = field_attach_view($entity_type, $entity); $output = drupal_render($entity->content); $variables = field_attach_preprocess($entity_type, $entity); - $this->assertTrue(isset($variables[$variable]), "Hidden field: variable $variable is available in templates."); + $this->assertTrue(isset($variables[$language][$variable]), "Hidden field: variable $variable is available in templates."); $this->content = $output; $this->assertNoRaw($this->instance['label'], "Hidden field: label is not displayed."); foreach ($values as $delta => $value) { @@ -288,7 +292,7 @@ } $this->content = $output; $this->assertRaw($display, "Multiple formatter: all values are displayed, formatter settings are applied."); - $this->content = $variables[$variable]; + $this->content = $variables[$language][$variable]; $this->assertRaw($display, "Multiple formatter: all values are displayed, formatter settings are applied (template variable)."); // TODO: @@ -299,30 +303,31 @@ function testFieldAttachDelete() { $entity_type = 'test_entity'; $rev[0] = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $language = FIELD_LANGUAGE_NEUTRAL; // Create revision 0 $values = array(); for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { $values[$delta]['value'] = mt_rand(1, 127); } - $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. @@ -330,13 +335,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]); @@ -347,7 +352,7 @@ } $read = field_test_create_stub_entity(0, 2, $this->instance['bundle']); field_attach_load($entity_type, array(0 => $read)); - $this->assertIdentical($read->{$this->field_name}, array(), "The test object current revision is deleted."); + $this->assertIdentical($read->{$this->field_name}, array(), "The test object current revision is deleted."); } function testFieldAttachCreateRenameBundle() { @@ -355,6 +360,7 @@ // hook_fieldable_info() is consistent. $new_bundle = 'test_bundle_' . drupal_strtolower($this->randomName()); field_test_create_bundle($new_bundle, $this->randomName()); + $language = FIELD_LANGUAGE_NEUTRAL; // Add an instance to that bundle. $this->instance['bundle'] = $new_bundle; @@ -366,14 +372,14 @@ for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { $values[$delta]['value'] = mt_rand(1, 127); } - $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. @@ -387,7 +393,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() { @@ -423,16 +429,17 @@ for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { $values[$delta]['value'] = mt_rand(1, 127); } - $entity->{$this->field_name} = $values; - $entity->{$field_name} = array(0 => array('value' => 99)); + $language = FIELD_LANGUAGE_NEUTRAL; + $entity->{$this->field_name}[$language] = $values; + $entity->{$field_name}[$language] = array(0 => array('value' => 99)); $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. @@ -441,8 +448,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"); @@ -451,12 +458,13 @@ function testFieldAttachCache() { // Create a revision + $language = FIELD_LANGUAGE_NEUTRAL; $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); $values = array(); for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { $values[$delta]['value'] = mt_rand(1, 127); } - $entity->{$this->field_name} = $values; + $entity->{$this->field_name}[$language] = $values; $noncached_type = 'test_entity'; $cached_type = 'test_cacheable_entity'; @@ -488,7 +496,7 @@ // Load, and confirm cache entry field_attach_load($cached_type, array(0 => $entity)); $cache = cache_get($cid, 'cache_field'); - $this->assertEqual($cache->data[$this->field_name], $values, 'Cached: correct cache entry on load'); + $this->assertEqual($cache->data[$this->field_name][$language], $values, 'Cached: correct cache entry on load'); // Delete, and confirm no cache entry field_attach_delete($cached_type, $entity); @@ -500,6 +508,7 @@ function testFieldAttachValidate() { $entity_type = 'test_entity'; $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $language = FIELD_LANGUAGE_NEUTRAL; // Set up values to generate errors $values = array(); @@ -509,7 +518,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); @@ -540,10 +549,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_NEUTRAL; + $this->assertEqual($form['field_translations'][$language][$this->field_name]['#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['field_translations'][$language][$this->field_name][$delta]['value']['#type'], 'textfield', "Form delta $delta widget is textfield"); } } @@ -570,7 +580,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_NEUTRAL; + $form_state['values'] = array('field_translations' => array($language => array($this->field_name => $values))); field_attach_submit($entity_type, $entity, $form, $form_state); asort($weights); @@ -580,7 +591,115 @@ $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'); + } +} + +/** + * 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_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('field_test', 'locale'); + + $this->field_name = drupal_strtolower($this->randomName() . '_field_name'); + + $this->field = array( + 'field_name' => $this->field_name, + 'type' => 'test_field', + 'cardinality' => 4, + 'translatable' => TRUE, + ); + 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); + + require_once DRUPAL_ROOT . '/includes/locale.inc'; + for ($i = 0; $i < 3; ++$i) { + locale_add_language('l'.$i, $this->randomString(), $this->randomString()); + } + } + + /** + * Check that that only the correct values are returned by _field_available_languages. + */ + function testFieldAvailableLanguages() { + // Test hook_field_languages invocation on a translatable field. + $enabled_languages = array_keys(language_list()); + $available_languages = _field_available_languages($this->field, $this->instance); + foreach ($available_languages as $language) { + $this->assertTrue(in_array($language, $enabled_languages), t('%language is an enabled language', array('%language' => $language))); + } + $this->assertTrue(count($available_languages) == count($enabled_languages) - 1, t('An enabled language was successfully made unavailable')); + + // Test _field_available_languages behavior for untranslatable fields. + $this->field['translatable'] = FALSE; + $this->field_name = $this->field['field_name'] = $this->instance['field_name'] = 'ut_test_field'; + $available_languages = _field_available_languages($this->field, $this->instance); + $this->assertTrue(count($available_languages) == 1 && $available_languages[0] === FIELD_LANGUAGE_NEUTRAL, t('For untranslatable fields only neutral language is 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_available_languages + $values = array(); + $extra_languages = mt_rand(1, 4); + $languages = $available_languages = _field_available_languages($this->field, $this->instance); + 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) { + $sign = 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 + // and if its result has been correctly stored by language in the returned data structure. + $this->assertEqual($sign, $result[0], 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')); } } Index: modules/field/field.default.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.default.inc,v retrieving revision 1.6 diff -u -r1.6 field.default.inc --- modules/field/field.default.inc 1 May 2009 15:28:13 -0000 1.6 +++ modules/field/field.default.inc 10 May 2009 22:58:47 -0000 @@ -11,21 +11,21 @@ * 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_translations'][$language][$field_name])) { + $items = $form_state['values']['field_translations'][$language][$field_name]; // Remove the 'value' of the 'add more' button. unset($items[$field_name . '_add_more']); } } -function field_default_validate($obj_type, $object, $field, $instance, $items) { +function field_default_validate($obj_type, $object, $field, $instance, $language, $items) { // TODO: here we could validate that required fields are filled in (for programmatic save) } -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']; // TODO: should me move what's below to __extract_form_values ? @@ -46,7 +46,7 @@ * 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'])) { @@ -112,7 +112,7 @@ * ), * ); */ -function field_default_view($obj_type, $object, $field, $instance, $items, $teaser) { +function field_default_view($obj_type, $object, $field, $instance, $language, $items, $teaser) { list($id, $vid, $bundle) = field_attach_extract_ids($obj_type, $object); $addition = array(); @@ -242,13 +242,14 @@ } } -function field_default_preprocess($obj_type, $object, $field, $instance, &$items) { +function field_default_preprocess($obj_type, $object, $field, $instance, $language, &$items) { return array( - $field['field_name'] . '_rendered' => isset($object->content[$field['field_name']]['#children']) ? $object->content[$field['field_name']]['#children'] : '', + $field['field_name'] . '_rendered' => isset($object->content['field_translations'][$language][$field['field_name']]['#children']) ? + $object->content['field_translations'][$language][$field['field_name']]['#children'] : '', ); } -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.crud.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.crud.inc,v retrieving revision 1.11 diff -u -r1.11 field.crud.inc --- modules/field/field.crud.inc 30 Apr 2009 14:40:03 -0000 1.11 +++ modules/field/field.crud.inc 10 May 2009 22:58:47 -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) @@ -212,6 +214,7 @@ $field += array( 'cardinality' => 1, + 'translatable' => FALSE, 'locked' => FALSE, 'settings' => array(), ); Index: modules/field/modules/text/text.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/text/text.module,v retrieving revision 1.6 diff -u -r1.6 text.module --- modules/field/modules/text/text.module 12 Apr 2009 02:18:51 -0000 1.6 +++ modules/field/modules/text/text.module 10 May 2009 22:58:50 -0000 @@ -92,7 +92,7 @@ * Possible error codes: * - 'text_max_length': The value 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) { if (!empty($item['value'])) { if (!empty($field['settings']['max_length']) && drupal_strlen($item['value']) > $field['settings']['max_length']) { @@ -105,7 +105,7 @@ } } -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) { // TODO D7 : this code is really node-related. Index: modules/field/modules/text/text.test =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/text/text.test,v retrieving revision 1.6 diff -u -r1.6 text.test --- modules/field/modules/text/text.test 29 Apr 2009 12:08:28 -0000 1.6 +++ modules/field/modules/text/text.test 10 May 2009 22:58:50 -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_NEUTRAL; 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"); 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.6 diff -u -r1.6 field_sql_storage.module --- modules/field/modules/field_sql_storage/field_sql_storage.module 30 Mar 2009 03:44:55 -0000 1.6 +++ modules/field/modules/field_sql_storage/field_sql_storage.module 10 May 2009 22:58:49 -0000 @@ -128,8 +128,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' ); @@ -144,7 +152,7 @@ $revision = $current; $revision['description'] = 'Revision archive storage for field ' . $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['field_name']) => $current, @@ -187,7 +195,7 @@ list($id, $vid, $bundle) = field_attach_extract_ids($obj_type, $obj); foreach (field_info_instances($bundle) as $instance) { $field_ids[$instance['field_name']][] = $load_current ? $id : $vid; - $delta_count[$id][$instance['field_name']] = 0; + $delta_count[$id][$instance['field_name']] = array(); } } @@ -209,7 +217,11 @@ ->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. @@ -217,9 +229,9 @@ $item[$column] = $row->{_field_sql_storage_columnname($field_name, $column)}; } - // Add the item to the field values for the entity. - $additions[$row->entity_id][$field_name][] = $item; - $delta_count[$row->entity_id][$field_name]++; + // Add the item to the field values for the entity according to its language. + $additions[$row->entity_id][$field_name][$row->language][$row->delta] = $item; + $delta_count[$row->entity_id][$field_name][$row->language]++; } } } @@ -244,58 +256,59 @@ $revision_name = _field_sql_storage_revision_tablename($field_name); $field = field_read_field($field_name); - // Leave the field untouched if $object comes with no $field_name property. - // Empty the field if $object->$field_name is NULL or an empty array. - - // 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)) { + // Leave the field untouched if $object comes with an empty $field_name property. + // Empty the field translations if $object->$field_name[$language] is NULL or an empty array. + if (isset($object->$field_name) && is_array($object->$field_name) && count($object->$field_name) > 0) { // 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(); + $languages = array_keys($object->$field_name); + 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) { - // Prepare the multi-insert query. - $columns = array('etid', 'entity_id', 'revision_id', 'bundle', 'delta'); - foreach ($field['columns'] as $column => $attributes) { - $columns[] = _field_sql_storage_columnname($field_name, $column); - } - $query = db_insert($table_name)->fields($columns); - if (isset($vid)) { - $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); - } + // Prepare the multi-insert query. + $columns = array('etid', 'entity_id', 'revision_id', 'bundle', 'delta', 'language'); + foreach ($field['columns'] as $column => $attributes) { + $columns[] = _field_sql_storage_columnname($field_name, $column); + } + $query = db_insert($table_name)->fields($columns); + if (isset($vid)) { + $revision_query = db_insert($revision_name)->fields($columns); + } - if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED && ++$delta_count == $field['cardinality']) { - break; + foreach ($object->$field_name as $language => $items) { + if ($items) { + $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; + } } } + } - // Execute the insert. - $query->execute(); - if (isset($vid)) { - $revision_query->execute(); - } + // Execute the insert. + $query->execute(); + if (isset($vid)) { + $revision_query->execute(); } } } 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.3 diff -u -r1.3 field_sql_storage.test --- modules/field/modules/field_sql_storage/field_sql_storage.test 29 Apr 2009 21:33:00 -0000 1.3 +++ modules/field/modules/field_sql_storage/field_sql_storage.test 10 May 2009 22:58:50 -0000 @@ -56,9 +56,10 @@ function testFieldAttachLoad() { $entity_type = 'test_entity'; $eid = 0; + $language = FIELD_LANGUAGE_NEUTRAL; $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,10 +99,10 @@ 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."); } } } @@ -114,6 +115,7 @@ function testFieldAttachInsertAndUpdate() { $entity_type = 'test_entity'; $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $language = FIELD_LANGUAGE_NEUTRAL; // Test insert. $values = array(); @@ -122,7 +124,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 +144,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 +172,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 +184,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 +196,7 @@ function testFieldAttachSaveMissingData() { $entity_type = 'test_entity'; $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $language = FIELD_LANGUAGE_NEUTRAL; // Insert: Field is missing field_attach_insert($entity_type, $entity); @@ -201,25 +204,25 @@ $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_result(db_query("SELECT COUNT(*) FROM {{$this->table}}")); $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_result(db_query("SELECT COUNT(*) FROM {{$this->table}}")); $this->assertEqual($count, 1, 'Field data saved'); - // Update: Field is missing. Data should survive. - unset($entity->{$this->field_name}); + // Update: Field translation is missing. Data should survive. + unset($entity->{$this->field_name}[$language]); field_attach_update($entity_type, $entity); $count = db_result(db_query("SELECT COUNT(*) FROM {{$this->table}}")); $this->assertEqual($count, 1, 'Missing field leaves data in table'); // Update: Field is NULL. Data should be wiped. - $entity->{$this->field_name} = NULL; + $entity->{$this->field_name}[$language] = NULL; field_attach_update($entity_type, $entity); $count = db_result(db_query("SELECT COUNT(*) FROM {{$this->table}}")); $this->assertEqual($count, 0, 'NULL field leaves no data in table'); Index: modules/simpletest/tests/field_test.module =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/field_test.module,v retrieving revision 1.5 diff -u -r1.5 field_test.module --- modules/simpletest/tests/field_test.module 1 May 2009 19:17:46 -0000 1.5 +++ modules/simpletest/tests/field_test.module 10 May 2009 22:58:51 -0000 @@ -345,7 +345,7 @@ * 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( @@ -359,7 +359,7 @@ /** * Implementation of 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; @@ -430,8 +430,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 @@ -530,4 +530,21 @@ */ function field_test_default_value($obj_type, $object, $field, $instance) { return array(array('value' => 99)); -} \ No newline at end of file +} + +/** + * Generic op to test _field_invoke behavior. + */ +function field_test_field_test_op($obj_type, $object, $field, $instance, $language, &$items) { + return md5(serialize(array($obj_type, $object, $field['field_name'], $language, $items))); +} + +/** + * Implementation of hook_field_languages + */ +function field_test_field_languages($field, $instance, &$languages) { + // Add an unavailable language. + $languages[] = 'xx'; + // Remove an available language. + unset($languages[0]); +} Index: modules/field/modules/number/number.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/number/number.module,v retrieving revision 1.7 diff -u -r1.7 number.module --- modules/field/modules/number/number.module 29 Apr 2009 22:36:42 -0000 1.7 +++ modules/field/modules/number/number.module 10 May 2009 22:58:50 -0000 @@ -89,7 +89,7 @@ * - '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']) { Index: modules/field/modules/list/list.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/list/list.module,v retrieving revision 1.4 diff -u -r1.4 list.module --- modules/field/modules/list/list.module 13 Apr 2009 05:18:18 -0000 1.4 +++ modules/field/modules/list/list.module 10 May 2009 22:58:50 -0000 @@ -98,7 +98,7 @@ * 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'])) {