Index: modules/simpletest/drupal_web_test_case.php =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/drupal_web_test_case.php,v retrieving revision 1.117 diff -u -r1.117 drupal_web_test_case.php --- modules/simpletest/drupal_web_test_case.php 16 Jun 2009 04:43:47 -0000 1.117 +++ modules/simpletest/drupal_web_test_case.php 16 Jun 2009 18:03:46 -0000 @@ -639,7 +639,7 @@ protected function drupalCreateNode($settings = array()) { // Populate defaults array. $settings += array( - 'body' => array(array()), + 'body' => array(FIELD_LANGUAGE_NEUTRAL => array(array())), 'title' => $this->randomName(8), 'comment' => 2, 'changed' => REQUEST_TIME, @@ -676,7 +676,7 @@ 'value' => $this->randomName(32), 'format' => FILTER_FORMAT_DEFAULT ); - $settings['body'][0] += $body; + $settings['body'][FIELD_LANGUAGE_NEUTRAL][0] += $body; $node = (object) $settings; node_save($node); Index: modules/field/field.form.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.form.inc,v retrieving revision 1.10 diff -u -r1.10 field.form.inc --- modules/field/field.form.inc 2 Jun 2009 13:47:25 -0000 1.10 +++ modules/field/field.form.inc 16 Jun 2009 18:03:36 -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) { @@ -97,7 +97,14 @@ '#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 => $form_element, + ); + $form['#fields'][$field['field_name']]['form_path'] = array($field['field_name']); } @@ -287,9 +294,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); @@ -301,10 +308,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); @@ -332,7 +339,7 @@ // Make the changes we want to the form state. $field_name = $form_state['clicked_button']['#field_name']; 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(current($form_state['values'][$field_name])); } } } @@ -364,7 +371,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; } @@ -393,20 +400,21 @@ $form_state['values'] = $form_state_copy['values']; // Reset cached ids, so that they don't affect the actual form we output. drupal_static_reset('form_clean_id'); + $language = key($_POST[$field_name]); // 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'); @@ -423,8 +431,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.attach.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.attach.inc,v retrieving revision 1.22 diff -u -r1.22 field.attach.inc --- modules/field/field.attach.inc 7 Jun 2009 00:00:57 -0000 1.22 +++ modules/field/field.attach.inc 16 Jun 2009 18:03:35 -0000 @@ -173,6 +173,7 @@ // Merge default options. $default_options = array( 'default' => FALSE, + 'languages' => NULL ); $options += $default_options; @@ -185,32 +186,37 @@ // 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(); - // 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_available_languages($field, $instance, $options['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; - } } } @@ -255,6 +261,7 @@ // Merge default options. $default_options = array( 'default' => FALSE, + 'languages' => NULL ); $options += $default_options; @@ -278,10 +285,10 @@ } // 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]; + foreach (_field_available_languages($fields[$field_name], $instance, $options['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. @@ -292,17 +299,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; + } } } } @@ -311,8 +321,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]; + } } } } @@ -349,6 +361,53 @@ } /** + * TODO + */ +function _field_available_languages($field, $instance, $suggested_languages = NULL) { + static $field_languages; + + $field_name = $field['field_name']; + if (!isset($field_languages[$field_name])) { + if ($field['translatable']) { + $available_languages = array_keys(language_list()); + // The returned languages are a subset of the intersection of enabled ones and suggested ones. + $languages = is_array($suggested_languages) ? $available_languages = array_intersect($available_languages, $suggested_languages) : $available_languages; + // 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_values(array_intersect($available_languages, $languages)); + } + else { + $field_languages[$field_name] = array(FIELD_LANGUAGE_NEUTRAL); + } + } + + return $field_languages[$field_name]; +} + +/** + * TODO + * @param $requested_language + */ +function _field_validate_language($requested_language) { + // TODO: This should use a content language default instead of an UI one. + global $language; + return empty($requested_language) ? $language->language : $requested_language; +} + + +/** * Add form elements for all fields for an object to a form structure. * * @param $obj_type @@ -360,13 +419,19 @@ * 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) { +function field_attach_form($obj_type, $object, &$form, $form_state, $language = NULL) { + // Ensure that a valid language is used. + $options = array('languages' => array(_field_validate_language($language))); + // 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); + $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) { @@ -888,17 +953,19 @@ * @return * A structured content array tree for drupal_render(). */ -function field_attach_view($obj_type, $object, $teaser = FALSE) { +function field_attach_view($obj_type, $object, $teaser = FALSE, $language = NULL) { + // Ensure that a valid language is used. + $options = array('languages' => array(_field_validate_language($language))); + // Let field modules sanitize their data for output. - _field_invoke('sanitize', $obj_type, $object); + _field_invoke('sanitize', $obj_type, $object, $a, $b, $options); - $output = _field_invoke_default('view', $obj_type, $object, $teaser); + $output = _field_invoke_default('view', $obj_type, $object, $teaser, $b, $options); // Let other modules make changes after rendering the view. drupal_alter('field_attach_view', $output, $obj_type, $object, $teaser); return $output; - } /** @@ -908,8 +975,10 @@ * containing the themed output for the whole field. * - Adds the formatted values in the 'view' key of the items. */ -function field_attach_preprocess($obj_type, $object) { - return _field_invoke_default('preprocess', $obj_type, $object); +function field_attach_preprocess($obj_type, $object, $language = NULL) { + // Ensure that a valid language is used. + $options = array('languages' => array(_field_validate_language($language))); + return _field_invoke_default('preprocess', $obj_type, $object, $a, $b, $options); } /** Index: modules/field/field.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.module,v retrieving revision 1.13 diff -u -r1.13 field.module --- modules/field/field.module 6 Jun 2009 16:17:30 -0000 1.13 +++ modules/field/field.module 16 Jun 2009 18:03:36 -0000 @@ -66,6 +66,11 @@ /** * TODO */ +define('FIELD_LANGUAGE_NEUTRAL', '_0'); + +/** + * TODO + */ define('FIELD_BEHAVIOR_NONE', 0x0001); /** * TODO Index: modules/field/field.crud.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.crud.inc,v retrieving revision 1.15 diff -u -r1.15 field.crud.inc --- modules/field/field.crud.inc 12 Jun 2009 08:39:36 -0000 1.15 +++ modules/field/field.crud.inc 16 Jun 2009 18:03:36 -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) @@ -234,6 +236,7 @@ $field += array( 'cardinality' => 1, + 'translatable' => FALSE, 'locked' => FALSE, 'settings' => 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 16 Jun 2009 18:03:36 -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/field.test =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.test,v retrieving revision 1.27 diff -u -r1.27 field.test --- modules/field/field.test 16 Jun 2009 08:40:18 -0000 1.27 +++ modules/field/field.test 16 Jun 2009 18:03:38 -0000 @@ -58,6 +58,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_NEUTRAL; $entity_type = 'test_entity'; $values = array(); @@ -72,12 +73,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]); } } @@ -86,12 +87,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. @@ -99,12 +100,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))); } } } @@ -114,6 +115,7 @@ */ function testFieldAttachLoadMultiple() { $entity_type = 'test_entity'; + $language = FIELD_LANGUAGE_NEUTRAL; // Define 2 bundles. $bundles = array( @@ -156,7 +158,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); } @@ -167,9 +169,9 @@ $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))); } } } @@ -180,6 +182,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); @@ -187,28 +190,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(); @@ -217,17 +220,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')); } /** @@ -240,15 +243,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(); @@ -258,7 +262,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')); } /** @@ -266,6 +270,7 @@ */ function testFieldAttachQuery() { $cardinality = $this->field['cardinality']; + $language = FIELD_LANGUAGE_NEUTRAL; // Create an additional bundle with an instance of the field. field_test_create_bundle('test_bundle_1', 'Test Bundle 1'); @@ -284,13 +289,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. @@ -356,8 +361,10 @@ 'ftvid' => $entities[1]->ftvid, 'fttype' => $entities[1]->fttype, $this->field_name => array( - array('value' => $values[0], 'additional_key' => 'additional_value'), - array('value' => $common_value, 'additional_key' => 'additional_value'), + $language => array( + array('value' => $values[0], 'additional_key' => 'additional_value'), + array('value' => $common_value, 'additional_key' => 'additional_value'), + ), ), ), ), @@ -367,7 +374,9 @@ 'ftvid' => $entities[2]->ftvid, 'fttype' => $entities[2]->fttype, $this->field_name => array( - array('value' => $common_value, 'additional_key' => 'additional_value'), + $language => array( + array('value' => $common_value, 'additional_key' => 'additional_value'), + ), ), ), ), @@ -384,19 +393,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_NEUTRAL; $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. @@ -452,8 +462,10 @@ 'ftvid' => $entities[1]->ftvid, 'fttype' => $entities[1]->fttype, $this->field_name => array( - array('value' => $values[0], 'additional_key' => 'additional_value'), - array('value' => $common_value, 'additional_key' => 'additional_value'), + $language => array( + array('value' => $values[0], 'additional_key' => 'additional_value'), + array('value' => $common_value, 'additional_key' => 'additional_value'), + ), ), ), $entities[2]->ftvid => (object) array( @@ -461,7 +473,9 @@ 'ftvid' => $entities[2]->ftvid, 'fttype' => $entities[2]->fttype, $this->field_name => array( - array('value' => $common_value, 'additional_key' => 'additional_value'), + $language => array( + array('value' => $common_value, 'additional_key' => 'additional_value'), + ), ), ), ), @@ -472,10 +486,11 @@ 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 = $this->_generateTestFieldValues($this->field['cardinality']); - $entity->{$this->field_name} = $values; + $entity->{$this->field_name}[$language] = $values; // Simple formatter, label displayed. $formatter_setting = $this->randomName(); @@ -565,28 +580,29 @@ function testFieldAttachDelete() { $entity_type = 'test_entity'; + $language = FIELD_LANGUAGE_NEUTRAL; $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. @@ -594,13 +610,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]); @@ -626,15 +642,16 @@ // Save an object with data in the field. $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $language = FIELD_LANGUAGE_NEUTRAL; $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. @@ -648,7 +665,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() { @@ -680,17 +697,18 @@ // Save an object with data for both fields $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $language = FIELD_LANGUAGE_NEUTRAL; $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. @@ -699,8 +717,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"); @@ -713,6 +731,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_NEUTRAL; $values = $this->_generateTestFieldValues($this->field['cardinality']); $noncached_type = 'test_entity'; @@ -727,7 +746,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')); @@ -745,7 +764,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')); @@ -753,12 +772,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')); @@ -766,13 +785,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')); @@ -781,7 +800,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); @@ -793,6 +812,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(); @@ -802,7 +822,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); @@ -813,15 +833,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 @@ -833,10 +853,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[$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"); } } @@ -863,7 +884,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($this->field_name => array($language => $values)); field_attach_submit($entity_type, $entity, $form, $form_state); asort($weights); @@ -873,7 +895,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'); } /** @@ -1043,45 +1065,46 @@ $this->instance['field_name'] = $this->field_name; field_create_field($this->field); field_create_instance($this->instance); + $language = FIELD_LANGUAGE_NEUTRAL; // 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); @@ -1096,6 +1119,7 @@ $this->instance['required'] = TRUE; field_create_field($this->field); field_create_instance($this->instance); + $language = FIELD_LANGUAGE_NEUTRAL; // Submit with missing required value. $edit = array(); @@ -1104,17 +1128,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'); } @@ -1133,17 +1157,18 @@ $this->instance['field_name'] = $this->field_name; field_create_field($this->field); field_create_instance($this->instance); + $language = FIELD_LANGUAGE_NEUTRAL; // 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. @@ -1160,8 +1185,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; @@ -1173,15 +1198,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')); @@ -1191,7 +1216,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 @@ -1213,6 +1238,7 @@ $this->instance['field_name'] = $this->field_name; field_create_field($this->field); field_create_instance($this->instance); + $language = FIELD_LANGUAGE_NEUTRAL; // Display creation form -> 1 widget. $this->drupalGet('test-entity/add/test-bundle'); @@ -1234,8 +1260,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; @@ -1248,15 +1274,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'); } /** @@ -1489,17 +1515,18 @@ // Save an object with data for the field $entity = field_test_create_stub_entity(0, 0, $instance['bundle']); + $language = FIELD_LANGUAGE_NEUTRAL; $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"); } } } @@ -1668,3 +1695,180 @@ $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_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->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); + + 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. + $this->field['settings']['test_hook_in'] = TRUE; + $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->assertFalse(in_array('xx', $available_languages), t('No invalid language was made available')); + $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'] = drupal_strtolower($this->randomName() . '_field_name'); + $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 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_available_languages($this->field, $this->instance, $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_available_languages($this->field, $this->instance, $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_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) { + $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_available_languages($this->field, $this->instance); + + 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_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))); + } + } +} Index: modules/field/field.default.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.default.inc,v retrieving revision 1.7 diff -u -r1.7 field.default.inc --- modules/field/field.default.inc 3 Jun 2009 02:41:07 -0000 1.7 +++ modules/field/field.default.inc 16 Jun 2009 18:03:36 -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_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_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['field_name']]['#children']) ? + $object->content[$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/search/search.test =================================================================== RCS file: /cvs/drupal/drupal/modules/search/search.test,v retrieving revision 1.23 diff -u -r1.23 search.test --- modules/search/search.test 12 Jun 2009 08:39:38 -0000 1.23 +++ modules/search/search.test 16 Jun 2009 18:03:45 -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_NEUTRAL => 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_NEUTRAL][0]['value'] .= " really rocks"; break; case 'recent': $settings['created'] = REQUEST_TIME + 3600; Index: modules/dblog/dblog.test =================================================================== RCS file: /cvs/drupal/drupal/modules/dblog/dblog.test,v retrieving revision 1.21 diff -u -r1.21 dblog.test --- modules/dblog/dblog.test 12 Jun 2009 08:39:36 -0000 1.21 +++ modules/dblog/dblog.test 16 Jun 2009 18:03:34 -0000 @@ -327,7 +327,7 @@ default: $content = array( 'title' => $this->randomName(8), - 'body[0][value]' => $this->randomName(32), + 'body[' . FIELD_LANGUAGE_NEUTRAL . '][0][value]' => $this->randomName(32), ); break; } @@ -351,7 +351,7 @@ default: $content = array( - 'body[0][value]' => $this->randomName(32), + 'body[' . FIELD_LANGUAGE_NEUTRAL . '][0][value]' => $this->randomName(32), ); break; } Index: modules/trigger/trigger.test =================================================================== RCS file: /cvs/drupal/drupal/modules/trigger/trigger.test,v retrieving revision 1.12 diff -u -r1.12 trigger.test --- modules/trigger/trigger.test 12 Jun 2009 08:39:40 -0000 1.12 +++ modules/trigger/trigger.test 16 Jun 2009 18:03:48 -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_NEUTRAL . '][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/blog/blog.test =================================================================== RCS file: /cvs/drupal/drupal/modules/blog/blog.test,v retrieving revision 1.13 diff -u -r1.13 blog.test --- modules/blog/blog.test 12 Jun 2009 08:39:35 -0000 1.13 +++ modules/blog/blog.test 16 Jun 2009 18:03:33 -0000 @@ -154,7 +154,7 @@ // Edit blog node. $edit = array(); $edit['title'] = 'node/' . $node->nid; - $edit['body[0][value]'] = $this->randomName(256); + $edit['body[' . FIELD_LANGUAGE_NEUTRAL . '][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/field/modules/text/text.test =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/text/text.test,v retrieving revision 1.7 diff -u -r1.7 text.test --- modules/field/modules/text/text.test 12 Jun 2009 08:39:37 -0000 1.7 +++ modules/field/modules/text/text.test 16 Jun 2009 18:03:40 -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"); @@ -91,16 +92,17 @@ ) ); field_create_instance($this->instance); + $language = FIELD_LANGUAGE_NEUTRAL; // 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_NEUTRAL; // 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/field/modules/text/text.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/text/text.module,v retrieving revision 1.12 diff -u -r1.12 text.module --- modules/field/modules/text/text.module 12 Jun 2009 08:39:37 -0000 1.12 +++ modules/field/modules/text/text.module 16 Jun 2009 18:03:40 -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,12 +174,10 @@ // handled by text_field_sanitize(). $format = $item['format']; if (filter_format_allowcache($format)) { - // TODO D7 : this code is really node-related. $check = is_null($object) || (isset($object->build_mode) && $object->build_mode == NODE_BUILD_PREVIEW); - $lang = isset($object->language) ? $object->language : $language->language; - $items[$id][$delta]['safe'] = isset($item['value']) ? check_markup($item['value'], $format, $lang, $check, FALSE) : ''; + $items[$id][$delta]['safe'] = isset($item['value']) ? check_markup($item['value'], $format, $language, $check, FALSE) : ''; if ($field['type'] == 'text_with_summary') { - $items[$id][$delta]['safe_summary'] = isset($item['summary']) ? check_markup($item['summary'], $format, $lang, $check, FALSE) : ''; + $items[$id][$delta]['safe_summary'] = isset($item['summary']) ? check_markup($item['summary'], $format, $language, $check, FALSE) : ''; } } } @@ -200,7 +196,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/blogapi/blogapi.module =================================================================== RCS file: /cvs/drupal/drupal/modules/blogapi/blogapi.module,v retrieving revision 1.155 diff -u -r1.155 blogapi.module --- modules/blogapi/blogapi.module 12 Jun 2009 08:39:35 -0000 1.155 +++ modules/blogapi/blogapi.module 16 Jun 2009 18:03:34 -0000 @@ -213,7 +213,7 @@ } else { $edit['title'] = blogapi_blogger_title($content); - $edit['body'][0]['value'] = $content; + $edit['body'][FIELD_LANGUAGE_NEUTRAL][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_NEUTRAL][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_NEUTRAL][0]['value'] = $content; } module_invoke_all('node_blogapi_edit', $node); @@ -892,14 +892,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_NEUTRAL][0]['value'] = $struct['mt_excerpt'] . '' . $node->body[FIELD_LANGUAGE_NEUTRAL][0]['value']; } if ($struct['mt_text_more']) { - $node->body[0]['value'] = $node->body[0]['value'] . '' . $struct['mt_text_more']; + $node->body[FIELD_LANGUAGE_NEUTRAL][0]['value'] = $node->body[FIELD_LANGUAGE_NEUTRAL][0]['value'] . '' . $struct['mt_text_more']; } if ($struct['mt_convert_breaks']) { - $node->body[0]['format'] = $struct['mt_convert_breaks']; + $node->body[FIELD_LANGUAGE_NEUTRAL][0]['format'] = $struct['mt_convert_breaks']; } if ($struct['dateCreated']) { @@ -922,8 +922,8 @@ ); if ($bodies) { - $body = $node->body[0]['value']; - $format = $node->body[0]['format']; + $body = $node->body[FIELD_LANGUAGE_NEUTRAL][0]['value']; + $format = $node->body[FIELD_LANGUAGE_NEUTRAL][0]['format']; if ($node->comment == 1) { $comment = 2; } Index: modules/book/book.test =================================================================== RCS file: /cvs/drupal/drupal/modules/book/book.test,v retrieving revision 1.11 diff -u -r1.11 book.test --- modules/book/book.test 12 Jun 2009 08:39:36 -0000 1.11 +++ modules/book/book.test 16 Jun 2009 18:03:34 -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_NEUTRAL][0]['value'], $node->body[FIELD_LANGUAGE_NEUTRAL][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_NEUTRAL . '][0][value]'] = 'SimpleTest test body ' . $this->randomName(32) . ' ' . $this->randomName(32); $edit['book[bid]'] = $book_nid; if ($parent !== NULL) { Index: modules/path/path.test =================================================================== RCS file: /cvs/drupal/drupal/modules/path/path.test,v retrieving revision 1.13 diff -u -r1.13 path.test --- modules/path/path.test 12 Jun 2009 08:39:38 -0000 1.13 +++ modules/path/path.test 16 Jun 2009 18:03:44 -0000 @@ -212,7 +212,7 @@ $this->clickLink(t('add translation')); $edit = array(); $edit['title'] = $this->randomName(); - $edit['body[0][value]'] = $this->randomName(); + $edit['body[' . FIELD_LANGUAGE_NEUTRAL . '][0][value]'] = $this->randomName(); $edit['path'] = $this->randomName(); $this->drupalPost(NULL, $edit, t('Save')); Index: modules/field/modules/number/number.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/number/number.module,v retrieving revision 1.10 diff -u -r1.10 number.module --- modules/field/modules/number/number.module 28 May 2009 16:44:06 -0000 1.10 +++ modules/field/modules/number/number.module 16 Jun 2009 18:03:39 -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/field/modules/list/list.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/list/list.module,v retrieving revision 1.6 diff -u -r1.6 list.module --- modules/field/modules/list/list.module 27 May 2009 18:33:56 -0000 1.6 +++ modules/field/modules/list/list.module 16 Jun 2009 18:03:39 -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/php/php.test =================================================================== RCS file: /cvs/drupal/drupal/modules/php/php.test,v retrieving revision 1.12 diff -u -r1.12 php.test --- modules/php/php.test 12 Jun 2009 08:39:38 -0000 1.12 +++ modules/php/php.test 16 Jun 2009 18:03:44 -0000 @@ -23,7 +23,7 @@ * @return stdObject Node object. */ function createNodeWithCode() { - return $this->drupalCreateNode(array('body' => array('value' => ''))); + return $this->drupalCreateNode(array('body' => array(FIELD_LANGUAGE_NEUTRAL => 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_NEUTRAL . '][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/aggregator/aggregator.test =================================================================== RCS file: /cvs/drupal/drupal/modules/aggregator/aggregator.test,v retrieving revision 1.26 diff -u -r1.26 aggregator.test --- modules/aggregator/aggregator.test 12 Jun 2009 08:39:35 -0000 1.26 +++ modules/aggregator/aggregator.test 16 Jun 2009 18:03:33 -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_NEUTRAL . '][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.32 diff -u -r1.32 node.test --- modules/node/node.test 12 Jun 2009 08:39:38 -0000 1.32 +++ modules/node/node.test 16 Jun 2009 18:03:44 -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_NEUTRAL][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_NEUTRAL][0]['value'] == $reverted_node->body[FIELD_LANGUAGE_NEUTRAL][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_NEUTRAL . '][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_NEUTRAL . '][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_NEUTRAL . '][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_NEUTRAL . '][0][value]'] = $this->randomName(16); $this->drupalPost('node/add/page', $edit, t('Save')); // Check that the page has been created. @@ -458,7 +458,7 @@ // Create a node. $edit = array(); $edit['title'] = $this->randomName(8); - $edit['body[0][value]'] = $this->randomName(16); + $edit['body[' . FIELD_LANGUAGE_NEUTRAL . '][0][value]'] = $this->randomName(16); $this->drupalPost('node/add/page', $edit, t('Save')); // Check that the post information is displayed. @@ -479,7 +479,7 @@ // Create a node. $edit = array(); $edit['title'] = $this->randomName(8); - $edit['body[0][value]'] = $this->randomName(16); + $edit['body[' . FIELD_LANGUAGE_NEUTRAL . '][0][value]'] = $this->randomName(16); $this->drupalPost('node/add/page', $edit, t('Save')); // Check that the post information is displayed. @@ -644,7 +644,7 @@ $title = $this->randomName(8); $node = array( 'title' => $title, - 'body' => array(array('value' => $this->randomName(32))), + 'body' => array(FIELD_LANGUAGE_NEUTRAL => array(array('value' => $this->randomName(32)))), 'uid' => $this->web_user->uid, 'type' => 'article', 'nid' => $test_nid, Index: modules/node/node.module =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.module,v retrieving revision 1.1070 diff -u -r1.1070 node.module --- modules/node/node.module 12 Jun 2009 08:39:38 -0000 1.1070 +++ modules/node/node.module 16 Jun 2009 18:03:43 -0000 @@ -886,7 +886,7 @@ // Make sure the body has the minimum number of words. // TODO : use a better word counting algorithm that will work in other languages - if (!empty($type->min_word_count) && isset($node->body[0]['value']) && count(explode(' ', $node->body[0]['value'])) < $type->min_word_count) { + if (!empty($type->min_word_count) && isset($node->body[FIELD_LANGUAGE_NEUTRAL][0]['value']) && count(explode(' ', $node->body[FIELD_LANGUAGE_NEUTRAL][0]['value'])) < $type->min_word_count) { // TODO: Use Field API to set this error. form_set_error('body', t('The body of your @type is too short. You need at least %words words.', array('%words' => $type->min_word_count, '@type' => $type->name))); } Index: modules/node/node.install =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.install,v retrieving revision 1.23 diff -u -r1.23 node.install --- modules/node/node.install 12 Jun 2009 08:39:38 -0000 1.23 +++ modules/node/node.install 16 Jun 2009 18:03:42 -0000 @@ -481,15 +481,15 @@ 'type' => $revision->type, ); if (!empty($revision->teaser) && $revision->teaser != text_summary($revision->body)) { - $node->body[0]['summary'] = $revision->teaser; + $node->body[FIELD_LANGUAGE_NEUTRAL][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_NEUTRAL][0]['value'] = $revision->body; + $node->body[FIELD_LANGUAGE_NEUTRAL][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/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.14 diff -u -r1.14 field_sql_storage.module --- modules/field/modules/field_sql_storage/field_sql_storage.module 15 Jun 2009 10:10:46 -0000 1.14 +++ modules/field/modules/field_sql_storage/field_sql_storage.module 16 Jun 2009 18:03:38 -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])) { $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(); } } } @@ -233,7 +241,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. @@ -243,8 +255,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]++; } } } @@ -268,23 +280,32 @@ $table_name = _field_sql_storage_tablename($field); $revision_name = _field_sql_storage_revision_tablename($field); - // 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) + ->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) + ->execute(); } } if ($object->$field_name) { - // Prepare the multi-insert query. - $columns = array('etid', 'entity_id', 'revision_id', 'bundle', 'delta'); + // 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); } @@ -293,25 +314,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 ($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; + } + } } } @@ -419,10 +445,10 @@ if ($load_values) { // Populate actual field values. - if (!isset($delta_count[$row->type][$id])) { - $delta_count[$row->type][$id] = 0; + if (!isset($delta_count[$row->type][$id][$row->language])) { + $delta_count[$row->type][$id][$row->language] = 0; } - if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta_count[$row->type][$id] < $field['cardinality']) { + if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta_count[$row->type][$id][$row->language] < $field['cardinality']) { $item = array(); // For each column declared by the field, populate the item // from the prefixed database column. @@ -436,8 +462,8 @@ $return[$row->type][$id] = field_attach_create_stub_object($row->type, array($row->entity_id, $row->revision_id, $row->bundle)); } // Add the item to the field values for the entity. - $return[$row->type][$id]->{$field_name}[] = $item; - $delta_count[$row->type][$id]++; + $return[$row->type][$id]->{$field_name}[$row->language][] = $item; + $delta_count[$row->type][$id][$row->language]++; } } else { 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.4 diff -u -r1.4 field_sql_storage.test --- modules/field/modules/field_sql_storage/field_sql_storage.test 28 May 2009 10:05:32 -0000 1.4 +++ modules/field/modules/field_sql_storage/field_sql_storage.test 16 Jun 2009 18:03:39 -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/system/system.test =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.test,v retrieving revision 1.50 diff -u -r1.50 system.test --- modules/system/system.test 13 Jun 2009 21:08:31 -0000 1.50 +++ modules/system/system.test 16 Jun 2009 18:03:48 -0000 @@ -518,7 +518,7 @@ $edit = array( 'title' => $this->randomName(10), - 'body' => array(array('value' => $this->randomName(100))), + 'body' => array(FIELD_LANGUAGE_NEUTRAL => array(array('value' => $this->randomName(100)))), ); $node = $this->drupalCreateNode($edit); @@ -705,7 +705,7 @@ // Generate node content. $edit = array( 'title' => '!SimpleTest! ' . $title . $this->randomName(20), - 'body[0][value]' => '!SimpleTest! test body' . $this->randomName(200), + 'body[' . FIELD_LANGUAGE_NEUTRAL . '][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/forum/forum.test =================================================================== RCS file: /cvs/drupal/drupal/modules/forum/forum.test,v retrieving revision 1.21 diff -u -r1.21 forum.test --- modules/forum/forum.test 12 Jun 2009 13:59:56 -0000 1.21 +++ modules/forum/forum.test 16 Jun 2009 18:03:40 -0000 @@ -234,7 +234,7 @@ $edit = array( 'title' => $title, - 'body[0][value]' => $body, + 'body[' . FIELD_LANGUAGE_NEUTRAL . '][0][value]' => $body, 'taxonomy[1]' => $tid ); @@ -323,7 +323,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_NEUTRAL . '][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/locale/locale.test =================================================================== RCS file: /cvs/drupal/drupal/modules/locale/locale.test,v retrieving revision 1.26 diff -u -r1.26 locale.test --- modules/locale/locale.test 12 Jun 2009 08:39:38 -0000 1.26 +++ modules/locale/locale.test 16 Jun 2009 18:03:41 -0000 @@ -1395,7 +1395,7 @@ $edit = array( 'type' => 'page', 'title' => $node_title, - 'body' => array(array('value' => $node_body)), + 'body' => array(FIELD_LANGUAGE_NEUTRAL => array(array('value' => $node_body))), 'language' => $langcode, ); $node = $this->drupalCreateNode($edit); Index: modules/simpletest/tests/common.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/common.test,v retrieving revision 1.46 diff -u -r1.46 common.test --- modules/simpletest/tests/common.test 12 Jun 2009 08:39:39 -0000 1.46 +++ modules/simpletest/tests/common.test 16 Jun 2009 18:03:47 -0000 @@ -261,7 +261,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_NEUTRAL => array(array('value' => t('This tests the inline CSS!') . "", 'format' => 3))), // PHP filter. 'promote' => 1, ); $node = $this->drupalCreateNode($settings); Index: modules/simpletest/tests/field_test.module =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/field_test.module,v retrieving revision 1.9 diff -u -r1.9 field_test.module --- modules/simpletest/tests/field_test.module 27 May 2009 18:34:00 -0000 1.9 +++ modules/simpletest/tests/field_test.module 16 Jun 2009 18:03:47 -0000 @@ -371,10 +371,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'])), ); @@ -385,7 +385,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; @@ -456,8 +456,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 @@ -516,7 +516,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. @@ -575,3 +575,33 @@ function field_test_default_value($obj_type, $object, $field, $instance) { return array(array('value' => 99)); } + +/** + * 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; +} + +/** + * Implementation of hook_field_languages + */ +function field_test_field_languages($field, $instance, &$languages) { + if ($field['settings']['test_hook_in']) { + // Add an unavailable language. + $languages[] = 'xx'; + // Remove an available language. + unset($languages[0]); + } +} Index: modules/translation/translation.test =================================================================== RCS file: /cvs/drupal/drupal/modules/translation/translation.test,v retrieving revision 1.12 diff -u -r1.12 translation.test --- modules/translation/translation.test 12 Jun 2009 08:39:40 -0000 1.12 +++ modules/translation/translation.test 16 Jun 2009 18:03:48 -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_NEUTRAL . '][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_NEUTRAL . '][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_NEUTRAL . '][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_NEUTRAL . '][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_NEUTRAL . '][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/taxonomy/taxonomy.test =================================================================== RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.test,v retrieving revision 1.35 diff -u -r1.35 taxonomy.test --- modules/taxonomy/taxonomy.test 12 Jun 2009 13:59:56 -0000 1.35 +++ modules/taxonomy/taxonomy.test 16 Jun 2009 18:03:48 -0000 @@ -490,7 +490,7 @@ // Post an article. $edit = array(); $edit['title'] = $this->randomName(); - $edit['body[0][value]'] = $this->randomName(); + $edit['body[' . FIELD_LANGUAGE_NEUTRAL . '][0][value]'] = $this->randomName(); $edit['taxonomy[' . $this->vocabulary->vid . ']'] = $term1->tid; $this->drupalPost('node/add/article', $edit, t('Save')); @@ -532,7 +532,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_NEUTRAL . '][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/filter/filter.test =================================================================== RCS file: /cvs/drupal/drupal/modules/filter/filter.test,v retrieving revision 1.23 diff -u -r1.23 filter.test --- modules/filter/filter.test 12 Jun 2009 08:39:37 -0000 1.23 +++ modules/filter/filter.test 16 Jun 2009 18:03:40 -0000 @@ -109,8 +109,8 @@ $edit = array(); $edit['title'] = $this->randomName(); - $edit['body[0][value]'] = $body . '' . $extra_text . ''; - $edit['body[0][value_format]'] = $filtered; + $edit['body[' . FIELD_LANGUAGE_NEUTRAL . '][0][value]'] = $body . '' . $extra_text . ''; + $edit['body[' . FIELD_LANGUAGE_NEUTRAL . '][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.'));