Index: modules/aggregator/aggregator.test =================================================================== RCS file: /cvs/drupal/drupal/modules/aggregator/aggregator.test,v retrieving revision 1.29 diff -u -p -r1.29 aggregator.test --- modules/aggregator/aggregator.test 17 Aug 2009 19:14:40 -0000 1.29 +++ modules/aggregator/aggregator.test 21 Aug 2009 13:28:40 -0000 @@ -248,11 +248,12 @@ EOF; } function createSampleNodes() { + $langcode = FIELD_LANGUAGE_NONE; // Post 5 articles. for ($i = 0; $i < 5; $i++) { $edit = array(); $edit['title'] = $this->randomName(); - $edit['body[0][value]'] = $this->randomName(); + $edit["body[$langcode][0][value]"] = $this->randomName(); $this->drupalPost('node/add/article', $edit, t('Save')); } } Index: modules/blog/blog.test =================================================================== RCS file: /cvs/drupal/drupal/modules/blog/blog.test,v retrieving revision 1.17 diff -u -p -r1.17 blog.test --- modules/blog/blog.test 3 Aug 2009 03:04:33 -0000 1.17 +++ modules/blog/blog.test 21 Aug 2009 13:28:40 -0000 @@ -152,7 +152,8 @@ class BlogTestCase extends DrupalWebTest // Edit blog node. $edit = array(); $edit['title'] = 'node/' . $node->nid; - $edit['body[0][value]'] = $this->randomName(256); + $langcode = FIELD_LANGUAGE_NONE; + $edit["body[$langcode][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/blogapi/blogapi.module =================================================================== RCS file: /cvs/drupal/drupal/modules/blogapi/blogapi.module,v retrieving revision 1.159 diff -u -p -r1.159 blogapi.module --- modules/blogapi/blogapi.module 17 Aug 2009 19:14:40 -0000 1.159 +++ modules/blogapi/blogapi.module 21 Aug 2009 13:28:40 -0000 @@ -213,7 +213,7 @@ function blogapi_blogger_new_post($appke } else { $edit['title'] = blogapi_blogger_title($content); - $edit['body'][0]['value'] = $content; + $edit['body'][FIELD_LANGUAGE_NONE][0]['value'] = $content; } if (!node_access('create', $edit['type'])) { @@ -274,12 +274,12 @@ function blogapi_blogger_edit_post($appk // Check for bloggerAPI vs. metaWeblogAPI. if (is_array($content)) { $node->title = $content['title']; - $node->body[0]['value'] = $content['description']; + $node->body[FIELD_LANGUAGE_NONE][0]['value'] = $content['description']; _blogapi_mt_extra($node, $content); } else { $node->title = blogapi_blogger_title($content); - $node->body[0]['value'] = $content; + $node->body[FIELD_LANGUAGE_NONE][0]['value'] = $content; } module_invoke_all('node_blogapi_edit', $node); @@ -895,14 +895,14 @@ function _blogapi_mt_extra($node, $struc // Merge the 3 body sections (description, mt_excerpt, mt_text_more) into one body. if ($struct['mt_excerpt']) { - $node->body[0]['value'] = $struct['mt_excerpt'] . '' . $node->body[0]['value']; + $node->body[FIELD_LANGUAGE_NONE][0]['value'] = $struct['mt_excerpt'] . '' . $node->body[FIELD_LANGUAGE_NONE][0]['value']; } if ($struct['mt_text_more']) { - $node->body[0]['value'] = $node->body[0]['value'] . '' . $struct['mt_text_more']; + $node->body[FIELD_LANGUAGE_NONE][0]['value'] = $node->body[FIELD_LANGUAGE_NONE][0]['value'] . '' . $struct['mt_text_more']; } if ($struct['mt_convert_breaks']) { - $node->body[0]['format'] = $struct['mt_convert_breaks']; + $node->body[FIELD_LANGUAGE_NONE][0]['format'] = $struct['mt_convert_breaks']; } if ($struct['dateCreated']) { @@ -925,8 +925,8 @@ function _blogapi_get_post($node, $bodie ); if ($bodies) { - $body = $node->body[0]['value']; - $format = $node->body[0]['format']; + $body = $node->body[FIELD_LANGUAGE_NONE][0]['value']; + $format = $node->body[FIELD_LANGUAGE_NONE][0]['format']; if ($node->comment == 1) { $comment = 2; } Index: modules/book/book.test =================================================================== RCS file: /cvs/drupal/drupal/modules/book/book.test,v retrieving revision 1.13 diff -u -p -r1.13 book.test --- modules/book/book.test 20 Jul 2009 18:51:32 -0000 1.13 +++ modules/book/book.test 21 Aug 2009 13:28:40 -0000 @@ -141,7 +141,7 @@ class BookTestCase extends DrupalWebTest // Check printer friendly version. $this->drupalGet('book/export/html/' . $node->nid); $this->assertText($node->title, t('Printer friendly title found.')); - $this->assertRaw(check_markup($node->body[0]['value'], $node->body[0]['format']), t('Printer friendly body found.')); + $this->assertRaw(check_markup($node->body[FIELD_LANGUAGE_NONE][0]['value'], $node->body[FIELD_LANGUAGE_NONE][0]['format']), t('Printer friendly body found.')); $number++; } @@ -173,7 +173,8 @@ class BookTestCase extends DrupalWebTest $edit = array(); $edit['title'] = $number . ' - SimpleTest test node ' . $this->randomName(10); - $edit['body[0][value]'] = 'SimpleTest test body ' . $this->randomName(32) . ' ' . $this->randomName(32); + $langcode = FIELD_LANGUAGE_NONE; + $edit["body[$langcode][0][value]"] = 'SimpleTest test body ' . $this->randomName(32) . ' ' . $this->randomName(32); $edit['book[bid]'] = $book_nid; if ($parent !== NULL) { Index: modules/dblog/dblog.test =================================================================== RCS file: /cvs/drupal/drupal/modules/dblog/dblog.test,v retrieving revision 1.27 diff -u -p -r1.27 dblog.test --- modules/dblog/dblog.test 16 Aug 2009 17:57:44 -0000 1.27 +++ modules/dblog/dblog.test 21 Aug 2009 13:28:40 -0000 @@ -330,9 +330,10 @@ class DBLogTestCase extends DrupalWebTes break; default: + $langcode = FIELD_LANGUAGE_NONE; $content = array( 'title' => $this->randomName(8), - 'body[0][value]' => $this->randomName(32), + "body[$langcode][0][value]" => $this->randomName(32), ); break; } @@ -355,8 +356,9 @@ class DBLogTestCase extends DrupalWebTes break; default: + $langcode = FIELD_LANGUAGE_NONE; $content = array( - 'body[0][value]' => $this->randomName(32), + "body[$langcode][0][value]" => $this->randomName(32), ); break; } Index: modules/field/field.api.php =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.api.php,v retrieving revision 1.28 diff -u -p -r1.28 field.api.php --- modules/field/field.api.php 20 Aug 2009 10:56:33 -0000 1.28 +++ modules/field/field.api.php 21 Aug 2009 13:28:40 -0000 @@ -350,6 +350,8 @@ function hook_field_schema($field) { * The field structure for the operation. * @param $instances * Array of instance structures for $field for each object, keyed by object id. + * @param $langcode + * The language associated to $items. * @param $items * Array of field values already loaded for the objects, keyed by object id. * @param $age @@ -359,9 +361,7 @@ function hook_field_schema($field) { * Changes or additions to field values are done by altering the $items * parameter by reference. */ -function hook_field_load($obj_type, $objects, $field, $instances, &$items, $age) { - global $language; - +function hook_field_load($obj_type, $objects, $field, $instances, $langcode, &$items, $age) { foreach ($objects as $id => $object) { foreach ($items[$id] as $delta => $item) { if (!empty($instances[$id]['settings']['text_processing'])) { @@ -369,10 +369,9 @@ function hook_field_load($obj_type, $obj // handled by hook_field_sanitize(). $format = $item['format']; if (filter_format_allowcache($format)) { - $lang = isset($object->language) ? $object->language : $language->language; - $items[$id][$delta]['safe'] = isset($item['value']) ? check_markup($item['value'], $format, $lang, FALSE) : ''; + $items[$id][$delta]['safe'] = isset($item['value']) ? check_markup($item['value'], $format, $langcode, FALSE) : ''; if ($field['type'] == 'text_with_summary') { - $items[$id][$delta]['safe_summary'] = isset($item['summary']) ? check_markup($item['summary'], $format, $lang, FALSE) : ''; + $items[$id][$delta]['safe_summary'] = isset($item['summary']) ? check_markup($item['summary'], $format, $langcode, FALSE) : ''; } } } @@ -401,11 +400,12 @@ function hook_field_load($obj_type, $obj * The field structure for the operation. * @param $instance * The instance structure for $field on $object's bundle. + * @param $langcode + * The language associated to $items. * @param $items * $object->{$field['field_name']}, or an empty array if unset. */ -function hook_field_sanitize($obj_type, $object, $field, $instance, $items) { - global $language; +function hook_field_sanitize($obj_type, $object, $field, $instance, $langcode, &$items) { foreach ($items as $delta => $item) { // Only sanitize items which were not already processed inside // hook_field_load(), i.e. items with uncacheable text formats, or coming @@ -413,10 +413,9 @@ function hook_field_sanitize($obj_type, if (!isset($items[$delta]['safe'])) { if (!empty($instance['settings']['text_processing'])) { $format = $item['format']; - $lang = isset($object->language) ? $object->language : $language->language; - $items[$delta]['safe'] = isset($item['value']) ? check_markup($item['value'], $format, $lang) : ''; + $items[$delta]['safe'] = isset($item['value']) ? check_markup($item['value'], $format, $langcode) : ''; if ($field['type'] == 'text_with_summary') { - $items[$delta]['safe_summary'] = isset($item['summary']) ? check_markup($item['summary'], $format, $lang) : ''; + $items[$delta]['safe_summary'] = isset($item['summary']) ? check_markup($item['summary'], $format, $langcode) : ''; } } else { @@ -444,8 +443,10 @@ function hook_field_sanitize($obj_type, * The field structure for the operation. * @param $instance * The instance structure for $field on $object's bundle. + * @param $langcode + * The language associated to $items. * @param $items - * $object->{$field['field_name']}, or an empty array if unset. + * $object->{$field['field_name']}[$langcode], or an empty array if unset. * @param $errors * The array of errors, keyed by field name and by value delta, that have * already been reported for the object. The function should add its errors @@ -454,7 +455,7 @@ function hook_field_sanitize($obj_type, * - 'error': an error code (should be a string, prefixed with the module name) * - 'message': the human readable message to be displayed. */ -function hook_field_validate($obj_type, $object, $field, $instance, $items, &$errors) { +function hook_field_validate($obj_type, $object, $field, $instance, $langcode, &$items, &$errors) { foreach ($items as $delta => $item) { if (!empty($item['value'])) { if (!empty($field['settings']['max_length']) && drupal_strlen($item['value']) > $field['settings']['max_length']) { @@ -478,10 +479,12 @@ function hook_field_validate($obj_type, * The field structure for the operation. * @param $instance * The instance structure for $field on $object's bundle. + * @param $langcode + * The language associated to $items. * @param $items - * $object->{$field['field_name']}, or an empty array if unset. + * $object->{$field['field_name']}[$langcode], or an empty array if unset. */ -function hook_field_presave($obj_type, $object, $field, $instance, $items) { +function hook_field_presave($obj_type, $object, $field, $instance, $langcode, &$items) { } /** @@ -495,10 +498,12 @@ function hook_field_presave($obj_type, $ * The field structure for the operation. * @param $instance * The instance structure for $field on $object's bundle. + * @param $langcode + * The language associated to $items. * @param $items - * $object->{$field['field_name']}, or an empty array if unset. + * $object->{$field['field_name']}[$langcode], or an empty array if unset. */ -function hook_field_insert($obj_type, $object, $field, $instance, $items) { +function hook_field_insert($obj_type, $object, $field, $instance, $langcode, &$items) { } /** @@ -512,10 +517,12 @@ function hook_field_insert($obj_type, $o * The field structure for the operation. * @param $instance * The instance structure for $field on $object's bundle. + * @param $langcode + * The language associated to $items. * @param $items - * $object->{$field['field_name']}, or an empty array if unset. + * $object->{$field['field_name']}[$langcode], or an empty array if unset. */ -function hook_field_update($obj_type, $object, $field, $instance, $items) { +function hook_field_update($obj_type, $object, $field, $instance, $langcode, &$items) { } /** @@ -531,10 +538,12 @@ function hook_field_update($obj_type, $o * The field structure for the operation. * @param $instance * The instance structure for $field on $object's bundle. + * @param $langcode + * The language associated to $items. * @param $items - * $object->{$field['field_name']}, or an empty array if unset. + * $object->{$field['field_name']}[$langcode], or an empty array if unset. */ -function hook_field_delete($obj_type, $object, $field, $instance, $items) { +function hook_field_delete($obj_type, $object, $field, $instance, $langcode, &$items) { } /** @@ -551,10 +560,12 @@ function hook_field_delete($obj_type, $o * The field structure for the operation. * @param $instance * The instance structure for $field on $object's bundle. + * @param $langcode + * The language associated to $items. * @param $items - * $object->{$field['field_name']}, or an empty array if unset. + * $object->{$field['field_name']}[$langcode], or an empty array if unset. */ -function hook_field_delete_revision($obj_type, $object, $field, $instance, $items) { +function hook_field_delete_revision($obj_type, $object, $field, $instance, $langcode, &$items) { } /** @@ -570,10 +581,12 @@ function hook_field_delete_revision($obj * The field structure for the operation. * @param $instance * The instance structure for $field on $object's bundle. + * @param $langcode + * The language associated to $items. * @param $items - * $object->{$field['field_name']}, or an empty array if unset. + * $object->{$field['field_name']}[$langcode], or an empty array if unset. */ -function hook_field_prepare_translation($obj_type, $object, $field, $instance, $items) { +function hook_field_prepare_translation($obj_type, $object, $field, $instance, $langcode, &$items) { } /** @@ -902,7 +915,7 @@ function theme_field_formatter_FORMATTER * * See field_attach_form() for details and arguments. */ -function hook_field_attach_form($obj_type, $object, &$form, &$form_state) { +function hook_field_attach_form($obj_type, $object, &$form, &$form_state, $langcode) { } /** @@ -988,6 +1001,23 @@ function hook_field_attach_presave($obj_ } /** + * Act on field_attach_preprocess. + * + * This hook is invoked while preprocessing the field.tpl.php template file. + * + * @param $variables + * The variables array is passed by reference and will be populated with field values. + * @param $obj_type + * The type of $object; e.g. 'node' or 'user'. + * @param $object + * The object with fields to render. + * @param $element + * The structured array containing the values ready for rendering. + */ +function hook_field_attach_preprocess_alter(&$variables, $obj_type, $object, $element) { +} + +/** * Act on field_attach_insert. * * This hook allows modules to store data before the Field Storage @@ -1094,8 +1124,10 @@ function hook_field_attach_delete_revisi * The object with fields to render. * @param $build_mode * Build mode, e.g. 'full', 'teaser'... + * @param $langcode + * The language in which the field values will be displayed. */ -function hook_field_attach_view_alter($output, $obj_type, $object, $build_mode) { +function hook_field_attach_view_alter($output, $obj_type, $object, $build_mode, $langcode) { } /** Index: modules/field/field.attach.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.attach.inc,v retrieving revision 1.37 diff -u -p -r1.37 field.attach.inc --- modules/field/field.attach.inc 19 Aug 2009 13:31:12 -0000 1.37 +++ modules/field/field.attach.inc 21 Aug 2009 13:28:40 -0000 @@ -176,6 +176,7 @@ function _field_invoke($op, $obj_type, $ $default_options = array( 'default' => FALSE, 'deleted' => FALSE, + 'language' => NULL, ); $options += $default_options; @@ -196,32 +197,38 @@ function _field_invoke($op, $obj_type, $ // When in 'single field' mode, only act on the specified field. if ((!isset($options['field_id']) || $options['field_id'] == $instance['field_id']) && (!isset($options['field_name']) || $options['field_name'] == $field_name)) { $field = field_info_field($field_name); + $field_translations = array(); + $suggested_languages = empty($options['language']) ? NULL : array($options['language']); - // Extract the field values into a separate variable, easily accessed by - // hook implementations. - $items = isset($object->$field_name) ? $object->$field_name : array(); + // Initialize field translations according to the available languages. + foreach (field_multilingual_available_languages($obj_type, $field, $suggested_languages) as $langcode) { + $field_translations[$langcode] = isset($object->{$field_name}[$langcode]) ? $object->{$field_name}[$langcode] : 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 $langcode => $items) { + $result = $function($obj_type, $object, $field, $instance, $langcode, $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}[$langcode])) { + $object->{$field_name}[$langcode] = $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; - } } } @@ -273,6 +280,7 @@ function _field_invoke_multiple($op, $ob $default_options = array( 'default' => FALSE, 'deleted' => FALSE, + 'language' => NULL, ); $options += $default_options; @@ -314,7 +322,10 @@ function _field_invoke_multiple($op, $ob $grouped_objects[$field_id][$id] = $objects[$id]; // Extract the field values into a separate variable, easily accessed // by hook implementations. - $grouped_items[$field_id][$id] = isset($object->$field_name) ? $object->$field_name : array(); + $suggested_languages = empty($options['language']) ? NULL : array($options['language']); + foreach (field_multilingual_available_languages($obj_type, $fields[$field_id], $suggested_languages) as $langcode) { + $grouped_items[$field_id][$langcode][$id] = isset($object->{$field_name}[$langcode]) ? $object->{$field_name}[$langcode] : array(); + } } } // Initialize the return value for each object. @@ -326,17 +337,20 @@ function _field_invoke_multiple($op, $ob $field_name = $field['field_name']; $function = $options['default'] ? 'field_default_' . $op : $field['module'] . '_field_' . $op; if (drupal_function_exists($function)) { - $results = $function($obj_type, $grouped_objects[$field_id], $field, $grouped_instances[$field_id], $grouped_items[$field_id], $options, $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_id] as $langcode => $items) { + $results = $function($obj_type, $grouped_objects[$field_id], $field, $grouped_instances[$field_id], $langcode, $grouped_items[$field_id][$langcode], $options, $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; + } } } } @@ -345,8 +359,10 @@ function _field_invoke_multiple($op, $ob // 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_id] as $id => $object) { - if ($grouped_items[$field_id][$id] !== array() || property_exists($object, $field_name)) { - $object->$field_name = $grouped_items[$field_id][$id]; + foreach ($grouped_items[$field_id] as $langcode => $items) { + if ($grouped_items[$field_id][$langcode][$id] !== array() || isset($object->{$field_name}[$langcode])) { + $object->{$field_name}[$langcode] = $grouped_items[$field_id][$langcode][$id]; + } } } } @@ -394,6 +410,9 @@ function _field_invoke_multiple_default( * The form structure to fill in. * @param $form_state * An associative array containing the current state of the form. + * @param $langcode + * The language the field values are going to be entered, if no language + * is provided the default site language will be used. * @return * The form elements are added by reference at the top level of the $form * parameter. Sample structure: @@ -417,45 +436,57 @@ function _field_invoke_multiple_default( * // most common case), and will therefore be repeated as many times as * // needed, or 'multiple-values' (one single widget allows the input of * // several values, e.g checkboxes, select box...). + * // The sub-array is nested into a $langcode key where $langcode has the + * // same value of the $langcode parameter above. This allow us to match + * // the field data structure ($field_name[$langcode][$delta][$column]). + * // The '#language' key holds the same value of $langcode and it is used + * // to access the field sub-array when $langcode is unknown. * 'field_foo' => array( - * '#field_name' => the name of the field, * '#tree' => TRUE, - * '#required' => whether or not the field is required, - * '#title' => the label of the field instance, - * '#description' => the description text for the field instance, - * - * // Only for 'single' widgets: - * '#theme' => 'field_multiple_value_form', - * '#multiple' => the field cardinality, - * // One sub-array per copy of the widget, keyed by delta. - * 0 => array( - * '#title' => the title to be displayed by the widget, - * '#default_value' => the field value for delta 0, - * '#required' => whether the widget should be marked required, - * '#delta' => 0, + * '#language' => $langcode, + * $langcode => array( * '#field_name' => the name of the field, - * '#bundle' => the name of the bundle, - * '#columns' => the array of field columns, + * '#tree' => TRUE, + * '#required' => whether or not the field is required, + * '#title' => the label of the field instance, + * '#description' => the description text for the field instance, + * + * // Only for 'single' widgets: + * '#theme' => 'field_multiple_value_form', + * '#multiple' => the field cardinality, + * // One sub-array per copy of the widget, keyed by delta. + * 0 => array( + * '#title' => the title to be displayed by the widget, + * '#default_value' => the field value for delta 0, + * '#required' => whether the widget should be marked required, + * '#delta' => 0, + * '#field_name' => the name of the field, + * '#bundle' => the name of the bundle, + * '#columns' => the array of field columns, + * // The remaining elements in the sub-array depend on the widget. + * '#type' => the type of the widget, + * ... + * ), + * 1 => array( + * ... + * ), + * + * // Only for multiple widgets: + * '#bundle' => $instance['bundle'], + * '#columns' => array_keys($field['columns']), * // The remaining elements in the sub-array depend on the widget. * '#type' => the type of the widget, * ... * ), - * 1 => array( - * ... - * ), - * - * // Only for multiple widgets: - * '#bundle' => $instance['bundle'], - * '#columns' => array_keys($field['columns']), - * // The remaining elements in the sub-array depend on the widget. - * '#type' => the type of the widget, * ... * ), * ) * @endcode */ -function field_attach_form($obj_type, $object, &$form, &$form_state) { - $form += (array) _field_invoke_default('form', $obj_type, $object, $form, $form_state); +function field_attach_form($obj_type, $object, &$form, &$form_state, $langcode = NULL) { + // If no language is provided use the default site language. + $options = array('language' => field_multilingual_valid_language($langcode)); + $form += (array) _field_invoke_default('form', $obj_type, $object, $form, $form_state, $options); // Add custom weight handling. list($id, $vid, $bundle) = field_attach_extract_ids($obj_type, $object); @@ -991,6 +1022,9 @@ function field_attach_query_revisions($f * The object with fields to render. * @param $build_mode * Build mode, e.g. 'full', 'teaser'... + * @param $langcode + * The language the field values are to be shown in. If no language is + * provided the current language is used. * @return * A structured content array tree for drupal_render(). * Sample structure: @@ -1042,11 +1076,15 @@ function field_attach_query_revisions($f * ); * @endcode */ -function field_attach_view($obj_type, $object, $build_mode = 'full') { +function field_attach_view($obj_type, $object, $build_mode = 'full', $langcode = NULL) { + // If no language is provided use the current UI language. + $options = array('language' => field_multilingual_valid_language($langcode, FALSE)); + // Let field modules sanitize their data for output. - _field_invoke('sanitize', $obj_type, $object); + $null = NULL; + _field_invoke('sanitize', $obj_type, $object, $null, $null, $options); - $output = _field_invoke_default('view', $obj_type, $object, $build_mode); + $output = _field_invoke_default('view', $obj_type, $object, $build_mode, $null, $options); // Add custom weight handling. list($id, $vid, $bundle) = field_attach_extract_ids($obj_type, $object); @@ -1057,7 +1095,39 @@ function field_attach_view($obj_type, $o drupal_alter('field_attach_view', $output, $obj_type, $object, $build_mode); return $output; +} + +/** + * Populate the template variables with the field values available for rendering. + * + * The $variables array will be populated with all the field instance values + * associated with the given entity type, keyed by field name; in case of + * translatable fields the language currently chosen for display will be + * selected. + * + * @param $obj_type + * The type of $object; e.g. 'node' or 'user'. + * @param $object + * The object with fields to render. + * @param $element + * The structured array containing the values ready for rendering. + * @param $variables + * The variables array is passed by reference and will be populated with field + * values. + */ +function field_attach_preprocess($obj_type, $object, $element, &$variables) { + list(, , $bundle) = field_attach_extract_ids($obj_type, $object); + + foreach (field_info_instances($bundle) as $instance) { + $field_name = $instance['field_name']; + if (isset($element[$field_name]['#language'])) { + $langcode = $element[$field_name]['#language']; + $variables[$field_name] = isset($object->{$field_name}[$langcode]) ? $object->{$field_name}[$langcode] : NULL; + } + } + // Let other modules make changes to the $variables array. + drupal_alter('field_attach_preprocess', $variables, $obj_type, $object, $element); } /** Index: modules/field/field.crud.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.crud.inc,v retrieving revision 1.26 diff -u -p -r1.26 field.crud.inc --- modules/field/field.crud.inc 19 Aug 2009 13:31:12 -0000 1.26 +++ modules/field/field.crud.inc 21 Aug 2009 13:28:40 -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) @@ -237,6 +239,7 @@ function field_create_field($field) { $field += array( 'cardinality' => 1, + 'translatable' => FALSE, 'locked' => FALSE, 'settings' => array(), ); Index: modules/field/field.default.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.default.inc,v retrieving revision 1.15 diff -u -p -r1.15 field.default.inc --- modules/field/field.default.inc 19 Aug 2009 13:31:12 -0000 1.15 +++ modules/field/field.default.inc 21 Aug 2009 13:28:40 -0000 @@ -11,17 +11,17 @@ * the corresponding field_attach_[operation]() function. */ -function field_default_extract_form_values($obj_type, $object, $field, $instance, &$items, $form, &$form_state) { +function field_default_extract_form_values($obj_type, $object, $field, $instance, $langcode, &$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][$langcode])) { + $items = $form_state['values'][$field_name][$langcode]; // Remove the 'value' of the 'add more' button. unset($items[$field_name . '_add_more']); } } -function field_default_submit($obj_type, $object, $field, $instance, &$items, $form, &$form_state) { +function field_default_submit($obj_type, $object, $field, $instance, $langcode, &$items, $form, &$form_state) { $field_name = $field['field_name']; // Reorder items to account for drag-n-drop reordering. @@ -40,19 +40,25 @@ function field_default_submit($obj_type, * 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, $langcode, &$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 (empty($object) || !property_exists($object, $field['field_name'])) { - $items = field_get_default_value($obj_type, $object, $field, $instance); + // We also check that the current field translation is actually defined before + // assigning it a default value. This way we ensure that only the intended + // languages get a default value. Otherwise we could have default values for + // not yet open languages. + if (empty($object) || !property_exists($object, $field['field_name']) || + (isset($object->{$field['field_name']}[$langcode]) && count($object->{$field['field_name']}[$langcode]) == 0)) { + $items = field_get_default_value($obj_type, $object, $field, $instance, $langcode); } } + /** * Default field 'view' operation. * * @see field_attach_view() */ -function field_default_view($obj_type, $object, $field, $instance, $items, $build_mode) { +function field_default_view($obj_type, $object, $field, $instance, $langcode, $items, $build_mode) { list($id, $vid, $bundle) = field_attach_extract_ids($obj_type, $object); $addition = array(); @@ -82,6 +88,7 @@ function field_default_view($obj_type, $ '#label_display' => $label_display, '#build_mode' => $build_mode, '#single' => $single, + '#language' => $langcode, 'items' => array(), ); @@ -117,7 +124,7 @@ function field_default_view($obj_type, $ return $addition; } -function field_default_prepare_translation($obj_type, $object, $field, $instance, &$items) { +function field_default_prepare_translation($obj_type, $object, $field, $instance, $langcode, &$items) { $addition = array(); if (isset($object->translation_source->$field['field_name'])) { $addition[$field['field_name']] = $object->translation_source->$field['field_name']; Index: modules/field/field.form.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.form.inc,v retrieving revision 1.15 diff -u -p -r1.15 field.form.inc --- modules/field/field.form.inc 19 Aug 2009 13:31:12 -0000 1.15 +++ modules/field/field.form.inc 21 Aug 2009 13:29:10 -0000 @@ -9,7 +9,7 @@ /** * Create a separate form element for each field. */ -function field_default_form($obj_type, $object, $field, $instance, $items, &$form, &$form_state, $get_delta = NULL) { +function field_default_form($obj_type, $object, $field, $instance, $langcode, $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) { @@ -48,7 +48,7 @@ function field_default_form($obj_type, $ // and we are displaying an individual element, process the multiple value // form. if (!isset($get_delta) && field_behaviors_widget('multiple values', $instance) == FIELD_BEHAVIOR_DEFAULT) { - $form_element = field_multiple_value_form($field, $instance, $items, $form, $form_state); + $form_element = field_multiple_value_form($field, $instance, $langcode, $items, $form, $form_state); } // If the widget is handling multiple values (e.g Options), // or if we are displaying an individual element, just get a single form @@ -89,7 +89,20 @@ function field_default_form($obj_type, $ '#weight' => $instance['widget']['weight'], ); - $addition[$field['field_name']] = array_merge($form_element, $defaults); + $form_element = array_merge($form_element, $defaults); + + // Add the field form element as a child keyed by language code to match the + // field data structure: $object->{$field_name}[$langcode][$delta][$column]. + // The '#language' key can be used to access the field's form element when + // $langcode is unknown. The #weight property is inherited from the field's + // form element. + $addition[$field['field_name']] = array( + '#tree' => TRUE, + '#weight' => $form_element['#weight'], + '#language' => $langcode, + $langcode => $form_element, + ); + $form['#fields'][$field['field_name']]['form_path'] = array($field['field_name']); } @@ -104,7 +117,7 @@ function field_default_form($obj_type, $ * - AHAH-'add more' button * - drag-n-drop value reordering */ -function field_multiple_value_form($field, $instance, $items, &$form, &$form_state) { +function field_multiple_value_form($field, $instance, $langcode, $items, &$form, &$form_state) { $field = field_info_field($instance['field_name']); $field_name = $field['field_name']; @@ -197,9 +210,11 @@ function field_multiple_value_form($fiel '#field_name' => $field_name, '#bundle' => $instance['bundle'], '#attributes' => array('class' => 'field-add-more-submit'), + '#language' => $langcode, ); } } + return $form_element; } @@ -276,9 +291,9 @@ function theme_field_multiple_value_form /** * 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, $langcode, $items, $form, $errors) { $field_name = $field['field_name']; - if (!empty($errors[$field_name])) { + if (!empty($errors[$field_name][$langcode])) { $function = $instance['widget']['module'] . '_field_widget_error'; $function_exists = drupal_function_exists($function); @@ -290,10 +305,10 @@ function field_default_form_errors($obj_ } $multiple_widget = field_behaviors_widget('multiple values', $instance) != FIELD_BEHAVIOR_DEFAULT; - foreach ($errors[$field_name] as $delta => $delta_errors) { + foreach ($errors[$field_name][$langcode] 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[$langcode] : $element[$langcode][$delta]; foreach ($delta_errors as $error) { if ($function_exists) { $function($error_element, $error); @@ -320,8 +335,9 @@ function field_add_more_submit($form, &$ // Make the changes we want to the form state. $field_name = $form_state['clicked_button']['#field_name']; + $langcode = $form_state['clicked_button']['#language']; if ($form_state['values'][$field_name . '_add_more']) { - $form_state['field_item_count'][$field_name] = count($form_state['values'][$field_name]); + $form_state['field_item_count'][$field_name] = count($form_state['values'][$field_name][$langcode]); } } } @@ -383,19 +399,25 @@ function field_add_more_js($bundle_name, // Reset cached ids, so that they don't affect the actual form we output. drupal_static_reset('form_clean_id'); + // Ensure that a valid language is provided. + $langcode = key($_POST[$field_name]); + if ($langcode != FIELD_LANGUAGE_NONE) { + $langcode = field_multilingual_valid_language($langcode); + } + // 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][$langcode][$field['field_name'] . '_add_more']); + foreach ($_POST[$field_name][$langcode] as $delta => $item) { + $form_state['values'][$field_name][$langcode][$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][$langcode] = _field_sort_items($field, $form_state['values'][$field_name][$langcode]); + $_POST[$field_name][$langcode] = _field_sort_items($field, $_POST[$field_name][$langcode]); // 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][$langcode]) + 1); + $items = $form_state['values'][$field_name][$langcode]; + $form_element = field_default_form(NULL, NULL, $field, $instance, $langcode, $items, $form, $form_state); // Let other modules alter it. drupal_alter('form', $form_element, array(), 'field_add_more_js'); @@ -412,8 +434,8 @@ function field_add_more_js($bundle_name, // 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][$langcode])) + 1; + $_POST[$field_name][$langcode][$delta]['_weight'] = $delta; $form_state = form_state_defaults(); $form_state['input'] = $_POST; $form = form_builder($_POST['form_id'], $form, $form_state); Index: modules/field/field.info =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.info,v retrieving revision 1.4 diff -u -p -r1.4 field.info --- modules/field/field.info 8 Jun 2009 09:23:51 -0000 1.4 +++ modules/field/field.info 21 Aug 2009 13:28:40 -0000 @@ -9,6 +9,7 @@ files[] = field.install files[] = field.crud.inc files[] = field.info.inc files[] = field.default.inc +files[] = field.multilingual.inc files[] = field.attach.inc files[] = field.form.inc files[] = field.test Index: modules/field/field.info.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.info.inc,v retrieving revision 1.13 diff -u -p -r1.13 field.info.inc --- modules/field/field.info.inc 19 Aug 2009 13:31:12 -0000 1.13 +++ modules/field/field.info.inc 21 Aug 2009 13:28:40 -0000 @@ -130,6 +130,7 @@ function _field_info_collate_types($rese // Provide defaults. $fieldable_info += array( 'cacheable' => TRUE, + 'translation_handlers' => array(), 'bundles' => array(), ); $fieldable_info['object keys'] += array( Index: modules/field/field.install =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.install,v retrieving revision 1.11 diff -u -p -r1.11 field.install --- modules/field/field.install 13 Aug 2009 01:50:00 -0000 1.11 +++ modules/field/field.install 21 Aug 2009 13:28:40 -0000 @@ -63,6 +63,12 @@ function field_schema() { 'not null' => TRUE, 'default' => 0, ), + 'translatable' => array( + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), 'active' => array( 'type' => 'int', 'size' => 'tiny', Index: modules/field/field.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.module,v retrieving revision 1.24 diff -u -p -r1.24 field.module --- modules/field/field.module 19 Aug 2009 22:46:05 -0000 1.24 +++ modules/field/field.module 21 Aug 2009 13:28:40 -0000 @@ -12,6 +12,7 @@ */ require(DRUPAL_ROOT . '/modules/field/field.crud.inc'); require(DRUPAL_ROOT . '/modules/field/field.info.inc'); +require(DRUPAL_ROOT . '/modules/field/field.multilingual.inc'); require(DRUPAL_ROOT . '/modules/field/field.attach.inc'); /** @@ -67,6 +68,13 @@ require(DRUPAL_ROOT . '/modules/field/fi define('FIELD_CARDINALITY_UNLIMITED', -1); /** + * The language code assigned to untranslatable fields. + * + * Defined by ISO639-2 for "No linguistic content / Not applicable". + */ +define('FIELD_LANGUAGE_NONE', 'zxx'); + +/** * TODO */ define('FIELD_BEHAVIOR_NONE', 0x0001); @@ -274,13 +282,15 @@ function field_associate_fields($module) * The field structure. * @param $instance * The instance structure. + * @param $langcode + * The field language to fill-in with the default value. */ -function field_get_default_value($obj_type, $object, $field, $instance) { +function field_get_default_value($obj_type, $object, $field, $instance, $langcode = NULL) { $items = array(); if (!empty($instance['default_value_function'])) { $function = $instance['default_value_function']; if (drupal_function_exists($function)) { - $items = $function($obj_type, $object, $field, $instance); + $items = $function($obj_type, $object, $field, $instance, $langcode); } } elseif (!empty($instance['default_value'])) { @@ -699,6 +709,8 @@ function template_preprocess_field(&$var 'label' => check_plain(t($instance['label'])), 'label_display' => $element['#label_display'], 'field_empty' => $field_empty, + 'field_language' => $element['#language'], + 'field_translatable' => $field['translatable'], 'template_files' => array( 'field', 'field-' . $element['#field_name'], Index: modules/field/field.test =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.test,v retrieving revision 1.41 diff -u -p -r1.41 field.test --- modules/field/field.test 17 Aug 2009 07:12:16 -0000 1.41 +++ modules/field/field.test 21 Aug 2009 13:28:40 -0000 @@ -59,6 +59,7 @@ class FieldAttachTestCase extends Drupal // field_test_field_load() in field_test.module). $this->instance['settings']['test_hook_field_load'] = TRUE; field_update_instance($this->instance); + $langcode = FIELD_LANGUAGE_NONE; $entity_type = 'test_entity'; $values = array(); @@ -73,12 +74,12 @@ class FieldAttachTestCase extends Drupal $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}[$langcode] = $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}[$langcode] = $values[$revision_id]; field_attach_update($entity_type, $revision[$revision_id]); } } @@ -87,12 +88,12 @@ class FieldAttachTestCase extends Drupal $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}[$langcode]), $this->field['cardinality'], t('Current revision: expected number of values')); for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { // The field value loaded matches the one inserted or updated. - $this->assertEqual($entity->{$this->field_name}[$delta]['value'] , $values[$current_revision][$delta]['value'], t('Currrent revision: expected value %delta was found.', array('%delta' => $delta))); + $this->assertEqual($entity->{$this->field_name}[$langcode][$delta]['value'] , $values[$current_revision][$delta]['value'], t('Current 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}[$langcode][$delta]['additional_key'], 'additional_value', t('Current revision: extra information for value %delta was found', array('%delta' => $delta))); } // Confirm each revision loads the correct data. @@ -100,12 +101,12 @@ class FieldAttachTestCase extends Drupal $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}[$langcode]), $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}[$langcode][$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}[$langcode][$delta]['additional_key'], 'additional_value', t('Revision %revision_id: extra information for value %delta was found', array('%revision_id' => $revision_id, '%delta' => $delta))); } } } @@ -115,6 +116,7 @@ class FieldAttachTestCase extends Drupal */ function testFieldAttachLoadMultiple() { $entity_type = 'test_entity'; + $langcode = FIELD_LANGUAGE_NONE; // Define 2 bundles. $bundles = array( @@ -158,7 +160,7 @@ class FieldAttachTestCase extends Drupal $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($langcode => array(array('value' => $values[$index][$field_name]))); } field_attach_insert($entity_type, $entity); } @@ -169,17 +171,17 @@ class FieldAttachTestCase extends Drupal $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}[$langcode][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}[$langcode][0]['additional_key'], 'additional_value', t('Entity %index: extra information was found', array('%index' => $index))); } } // Check that the single-field load option works. $entity = field_test_create_stub_entity(1, 1, $bundles[1]); field_attach_load($entity_type, array(1 => $entity), FIELD_LOAD_CURRENT, array('field_id' => $field_ids[1])); - $this->assertEqual($entity->{$field_names[1]}[0]['value'], $values[1][$field_names[1]], t('Entity %index: expected value was found.', array('%index' => 1))); - $this->assertEqual($entity->{$field_names[1]}[0]['additional_key'], 'additional_value', t('Entity %index: extra information was found', array('%index' => 1))); + $this->assertEqual($entity->{$field_names[1]}[$langcode][0]['value'], $values[1][$field_names[1]], t('Entity %index: expected value was found.', array('%index' => 1))); + $this->assertEqual($entity->{$field_names[1]}[$langcode][0]['additional_key'], 'additional_value', t('Entity %index: extra information was found', array('%index' => 1))); $this->assert(!isset($entity->{$field_names[2]}), t('Entity %index: field %field_name is not loaded.', array('%index' => 2, '%field_name' => $field_names[2]))); $this->assert(!isset($entity->{$field_names[3]}), t('Entity %index: field %field_name is not loaded.', array('%index' => 3, '%field_name' => $field_names[3]))); } @@ -190,6 +192,7 @@ class FieldAttachTestCase extends Drupal function testFieldAttachSaveMissingData() { $entity_type = 'test_entity'; $entity_init = field_test_create_stub_entity(); + $langcode = FIELD_LANGUAGE_NONE; // Insert: Field is missing. $entity = clone($entity_init); @@ -197,28 +200,28 @@ class FieldAttachTestCase extends Drupal $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}[$langcode]), 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}[$langcode] = 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}[$langcode]), 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}[$langcode] = $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}[$langcode], $values, t('Field data saved')); // Update: Field is missing. Data should survive. field_cache_clear(); @@ -227,17 +230,17 @@ class FieldAttachTestCase extends Drupal $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}[$langcode], $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}[$langcode] = 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}[$langcode]), t('Update: NULL field removes existing values')); } /** @@ -250,15 +253,16 @@ class FieldAttachTestCase extends Drupal $entity_type = 'test_entity'; $entity_init = field_test_create_stub_entity(); + $langcode = FIELD_LANGUAGE_NONE; // Insert: Field is NULL. $entity = clone($entity_init); - $entity->{$this->field_name} = NULL; + $entity->{$this->field_name}[$langcode] = 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}[$langcode]), t('Insert: NULL field results in no value saved')); // Insert: Field is missing. field_cache_clear(); @@ -268,7 +272,7 @@ class FieldAttachTestCase extends Drupal $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}[$langcode], $values, t('Insert: missing field results in default value saved')); } /** @@ -276,6 +280,7 @@ class FieldAttachTestCase extends Drupal */ function testFieldAttachQuery() { $cardinality = $this->field['cardinality']; + $langcode = FIELD_LANGUAGE_NONE; // Create an additional bundle with an instance of the field. field_test_create_bundle('test_bundle_1', 'Test Bundle 1'); @@ -294,13 +299,13 @@ class FieldAttachTestCase extends Drupal $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}[$langcode][$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($langcode => array(array('value' => $common_value))); field_attach_insert($entity_types[2], $entities[2]); // Query on the object's values. @@ -364,7 +369,7 @@ class FieldAttachTestCase extends Drupal for ($i = 0; $i < 20; ++$i) { $offset_id += mt_rand(2, 5); $offset_entities[$offset_id] = field_test_create_stub_entity($offset_id, $offset_id, 'offset_bundle'); - $offset_entities[$offset_id]->{$this->field_name}[0] = array('value' => $offset_id); + $offset_entities[$offset_id]->{$this->field_name}[$langcode][0] = array('value' => $offset_id); field_attach_insert('test_entity', $offset_entities[$offset_id]); } @@ -397,19 +402,20 @@ class FieldAttachTestCase extends Drupal // 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)); + $langcode = FIELD_LANGUAGE_NONE; $values = array(); for ($delta = 0; $delta < $cardinality; $delta++) { do { $value = mt_rand(1, 127); } while (in_array($value, $values)); $values[$delta] = $value; - $entities[1]->{$this->field_name}[$delta] = array('value' => $values[$delta]); + $entities[1]->{$this->field_name}[$langcode][$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}[$langcode][0] = array('value' => $common_value); field_attach_update($entity_type, $entities[2]); // Query on the object's values. @@ -452,10 +458,11 @@ class FieldAttachTestCase extends Drupal function testFieldAttachViewAndPreprocess() { $entity_type = 'test_entity'; $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $langcode = FIELD_LANGUAGE_NONE; // Populate values to be displayed. $values = $this->_generateTestFieldValues($this->field['cardinality']); - $entity->{$this->field_name} = $values; + $entity->{$this->field_name}[$langcode] = $values; // Simple formatter, label displayed. $formatter_setting = $this->randomName(); @@ -525,32 +532,45 @@ class FieldAttachTestCase extends Drupal // TODO: // - check display order with several fields + + // Preprocess template. + $variables = array(); + field_attach_preprocess($entity_type, $entity, $entity->content, $variables); + $result = TRUE; + foreach ($values as $delta => $item) { + if ($variables[$this->field_name][$delta]['value'] !== $item['value']) { + $result = FALSE; + break; + } + } + $this->assertTrue($result, t('Variable $@field_name correctly populated.', array('@field_name' => $this->field_name))); } function testFieldAttachDelete() { $entity_type = 'test_entity'; + $langcode = FIELD_LANGUAGE_NONE; $rev[0] = field_test_create_stub_entity(0, 0, $this->instance['bundle']); // Create revision 0 $values = $this->_generateTestFieldValues($this->field['cardinality']); - $rev[0]->{$this->field_name} = $values; + $rev[0]->{$this->field_name}[$langcode] = $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}[$langcode] = $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}[$langcode] = $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}[$langcode]), $this->field['cardinality'], "The test object revision $vid has {$this->field['cardinality']} values."); } // Delete revision 1, confirm the other two still load. @@ -558,13 +578,13 @@ class FieldAttachTestCase extends Drupal 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}[$langcode]), $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}[$langcode]), $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]); @@ -590,15 +610,16 @@ class FieldAttachTestCase extends Drupal // Save an object with data in the field. $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $langcode = FIELD_LANGUAGE_NONE; $values = $this->_generateTestFieldValues($this->field['cardinality']); - $entity->{$this->field_name} = $values; + $entity->{$this->field_name}[$langcode] = $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}[$langcode]), $this->field['cardinality'], "Data is retrieved for the new bundle"); // Rename the bundle. This has to be initiated by the module so that its // hook_fieldable_info() is consistent. @@ -612,7 +633,7 @@ class FieldAttachTestCase extends Drupal // 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}[$langcode]), $this->field['cardinality'], "Bundle name has been updated in the field storage"); } function testFieldAttachDeleteBundle() { @@ -644,17 +665,18 @@ class FieldAttachTestCase extends Drupal // Save an object with data for both fields $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $langcode = FIELD_LANGUAGE_NONE; $values = $this->_generateTestFieldValues($this->field['cardinality']); - $entity->{$this->field_name} = $values; - $entity->{$field_name} = $this->_generateTestFieldValues(1); + $entity->{$this->field_name}[$langcode] = $values; + $entity->{$field_name}[$langcode] = $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}[$langcode]), 4, 'First field got loaded'); + $this->assertEqual(count($entity->{$field_name}[$langcode]), 1, 'Second field got loaded'); // Delete the bundle. This has to be initiated by the module so that its // hook_fieldable_info() is consistent. @@ -663,8 +685,8 @@ class FieldAttachTestCase extends Drupal // 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}[$langcode]), 'No data for first field'); + $this->assertFalse(isset($entity->{$field_name}[$langcode]), '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"); @@ -677,6 +699,7 @@ class FieldAttachTestCase extends Drupal function testFieldAttachCache() { // Initialize random values and a test entity. $entity_init = field_test_create_stub_entity(1, 1, $this->instance['bundle']); + $langcode = FIELD_LANGUAGE_NONE; $values = $this->_generateTestFieldValues($this->field['cardinality']); $noncached_type = 'test_entity'; @@ -691,7 +714,7 @@ class FieldAttachTestCase extends Drupal // Save, and check that no cache entry is present. $entity = clone($entity_init); - $entity->{$this->field_name} = $values; + $entity->{$this->field_name}[$langcode] = $values; field_attach_insert($noncached_type, $entity); $this->assertFalse(cache_get($cid, 'cache_field'), t('Non-cached: no cache entry on insert')); @@ -709,7 +732,7 @@ class FieldAttachTestCase extends Drupal // Save, and check that no cache entry is present. $entity = clone($entity_init); - $entity->{$this->field_name} = $values; + $entity->{$this->field_name}[$langcode] = $values; field_attach_insert($cached_type, $entity); $this->assertFalse(cache_get($cid, 'cache_field'), t('Cached: no cache entry on insert')); @@ -723,12 +746,12 @@ class FieldAttachTestCase extends Drupal $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][$langcode], $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}[$langcode] = $values; field_attach_update($cached_type, $entity); $this->assertFalse(cache_get($cid, 'cache_field'), t('Cached: no cache entry on update')); @@ -736,13 +759,13 @@ class FieldAttachTestCase extends Drupal $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][$langcode], $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}[$langcode] = $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')); @@ -751,7 +774,7 @@ class FieldAttachTestCase extends Drupal $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][$langcode], $values, t('Cached: correct cache entry on load')); // Delete, and check that the cache entry is wiped. field_attach_delete($cached_type, $entity); @@ -763,6 +786,7 @@ class FieldAttachTestCase extends Drupal function testFieldAttachValidate() { $entity_type = 'test_entity'; $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $langcode = FIELD_LANGUAGE_NONE; // Set up values to generate errors $values = array(); @@ -772,7 +796,7 @@ class FieldAttachTestCase extends Drupal } // Arrange for item 1 not to generate an error $values[1]['value'] = 1; - $entity->{$this->field_name} = $values; + $entity->{$this->field_name}[$langcode] = $values; try { field_attach_validate($entity_type, $entity); @@ -783,15 +807,15 @@ class FieldAttachTestCase extends Drupal 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][$langcode][$delta][0]['error'], 'field_test_invalid', "Error set on value $delta"); + $this->assertEqual(count($errors[$this->field_name][$langcode][$delta]), 1, "Only one error set on value $delta"); + unset($errors[$this->field_name][$langcode][$delta]); } else { - $this->assertFalse(isset($errors[$this->field_name][$delta]), "No error set on value $delta"); + $this->assertFalse(isset($errors[$this->field_name][$langcode][$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][$langcode]), 0, 'No extraneous errors set'); } // Validate that FAPI elements are generated. This could be much @@ -803,10 +827,11 @@ class FieldAttachTestCase extends Drupal $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']}"); + $langcode = FIELD_LANGUAGE_NONE; + $this->assertEqual($form[$this->field_name][$langcode]['#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][$langcode][$delta]['value']['#type'], 'textfield', "Form delta $delta widget is textfield"); } } @@ -833,7 +858,8 @@ class FieldAttachTestCase extends Drupal // Leave an empty value. 'field_test' fields are empty if empty(). $values[1]['value'] = 0; - $form_state['values'] = array($this->field_name => $values); + $langcode = FIELD_LANGUAGE_NONE; + $form_state['values'] = array($this->field_name => array($langcode => $values)); field_attach_submit($entity_type, $entity, $form, $form_state); asort($weights); @@ -843,7 +869,7 @@ class FieldAttachTestCase extends Drupal $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}[$langcode], $expected_values, 'Submit filters empty values'); } /** @@ -1102,45 +1128,46 @@ class FieldFormTestCase extends DrupalWe $this->instance['field_name'] = $this->field_name; field_create_field($this->field); field_create_instance($this->instance); + $langcode = FIELD_LANGUAGE_NONE; // Display creation form. $this->drupalGet('test-entity/add/test-bundle'); - $this->assertFieldByName($this->field_name . '[0][value]', '', 'Widget is displayed'); - $this->assertNoField($this->field_name . '[1][value]', 'No extraneous widget is displayed'); + $this->assertFieldByName("{$this->field_name}[$langcode][0][value]", '', 'Widget is displayed'); + $this->assertNoField("{$this->field_name}[$langcode][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}[$langcode][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}[$langcode][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}[$langcode][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}[$langcode][0][value]", $value, 'Widget is displayed with the correct default value'); + $this->assertNoField("{$this->field_name}[$langcode][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}[$langcode][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}[$langcode][0]['value'], $value, 'Field value was updated'); // Empty the field. $value = ''; - $edit = array($this->field_name . '[0][value]' => $value); + $edit = array("{$this->field_name}[$langcode][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); @@ -1155,6 +1182,7 @@ class FieldFormTestCase extends DrupalWe $this->instance['required'] = TRUE; field_create_field($this->field); field_create_instance($this->instance); + $langcode = FIELD_LANGUAGE_NONE; // Submit with missing required value. $edit = array(); @@ -1163,17 +1191,17 @@ class FieldFormTestCase extends DrupalWe // Create an entity $value = mt_rand(1, 127); - $edit = array($this->field_name . '[0][value]' => $value); + $edit = array("{$this->field_name}[$langcode][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}[$langcode][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}[$langcode][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'); } @@ -1192,17 +1220,18 @@ class FieldFormTestCase extends DrupalWe $this->instance['field_name'] = $this->field_name; field_create_field($this->field); field_create_instance($this->instance); + $langcode = FIELD_LANGUAGE_NONE; // Display creation form -> 1 widget. $this->drupalGet('test-entity/add/test-bundle'); - $this->assertFieldByName($this->field_name . '[0][value]', '', 'Widget 1 is displayed'); - $this->assertNoField($this->field_name . '[1][value]', 'No extraneous widget is displayed'); + $this->assertFieldByName("{$this->field_name}[$langcode][0][value]", '', 'Widget 1 is displayed'); + $this->assertNoField("{$this->field_name}[$langcode][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}[$langcode][0][value]", '', 'Widget 1 is displayed'); + $this->assertFieldByName("{$this->field_name}[$langcode][1][value]", '', 'New widget is displayed'); + $this->assertNoField("{$this->field_name}[$langcode][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. @@ -1219,8 +1248,8 @@ class FieldFormTestCase extends DrupalWe } 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[$langcode][$delta][value]"] = $value; + $edit["$this->field_name[$langcode][$delta][_weight]"] = $weight; // We'll need three slightly different formats to check the values. $values[$weight] = $value; $field_values[$weight]['value'] = (string)$value; @@ -1232,15 +1261,15 @@ class FieldFormTestCase extends DrupalWe 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[$langcode][$delta][value]", $values[$delta], "Widget $delta is displayed and has the right value"); + $this->assertFieldByName("$this->field_name[$langcode][$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[$langcode][$delta][value]", '', "New widget is displayed"); + $this->assertFieldByName("$this->field_name[$langcode][$delta][_weight]", $delta, "New widget has the right weight"); + $this->assertNoField("$this->field_name[$langcode][" . ($delta + 1) . '][value]', 'No extraneous widget is displayed'); // Submit the form and create the entity. $this->drupalPost(NULL, $edit, t('Save')); @@ -1250,7 +1279,7 @@ class FieldFormTestCase extends DrupalWe $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}[$langcode], $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 @@ -1272,6 +1301,7 @@ class FieldFormTestCase extends DrupalWe $this->instance['field_name'] = $this->field_name; field_create_field($this->field); field_create_instance($this->instance); + $langcode = FIELD_LANGUAGE_NONE; // Display creation form -> 1 widget. $this->drupalGet('test-entity/add/test-bundle'); @@ -1293,8 +1323,8 @@ class FieldFormTestCase extends DrupalWe } 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[$langcode][$delta][value]"] = $value; + $edit["$this->field_name[$langcode][$delta][_weight]"] = $weight; // We'll need three slightly different formats to check the values. $values[$weight] = $value; $field_values[$weight]['value'] = (string)$value; @@ -1307,15 +1337,15 @@ class FieldFormTestCase extends DrupalWe 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[$langcode][$delta][value]", $values[$delta], "Widget $delta is displayed and has the right value"); + $this->assertFieldByName("$this->field_name[$langcode][$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[$langcode][$delta][value]", '', "New widget is displayed"); + $this->assertFieldByName("$this->field_name[$langcode][$delta][_weight]", $delta, "New widget has the right weight"); + $this->assertNoField("$this->field_name[$langcode][" . ($delta + 1) . '][value]', 'No extraneous widget is displayed'); } /** @@ -1599,17 +1629,18 @@ class FieldCrudTestCase extends DrupalWe // Save an object with data for the field $entity = field_test_create_stub_entity(0, 0, $instance['bundle']); + $langcode = FIELD_LANGUAGE_NONE; $values[0]['value'] = mt_rand(1, 127); - $entity->{$field['field_name']} = $values; + $entity->{$field['field_name']}[$langcode] = $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']}[$langcode]), 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']}[$langcode][$delta]['value'], $values[$delta]['value'], "Data in previously deleted field saves and loads correctly"); } } } @@ -1800,6 +1831,236 @@ class FieldInstanceCrudTestCase extends } /** + * Unit test class for the multilanguage fields logic. + * + * The following tests will check the multilanguage logic of _field_invoke() and + * that only the correct values are returned by + * field_multilingual_available_languages(). + */ +class FieldTranslationsTestCase extends DrupalWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Field translations tests', + 'description' => 'Test multilanguage fields logic.', + 'group' => 'Field', + ); + } + + function setUp() { + parent::setUp('locale', 'field_test'); + + $this->field_name = drupal_strtolower($this->randomName() . '_field_name'); + + $this->obj_type = 'test_entity'; + + $this->field = array( + 'field_name' => $this->field_name, + 'type' => 'test_field', + 'cardinality' => 4, + 'translatable' => TRUE, + 'settings' => array( + 'test_hook_in' => FALSE, + ), + ); + field_create_field($this->field); + + $this->instance = array( + 'field_name' => $this->field_name, + 'bundle' => 'test_bundle', + 'label' => $this->randomName() . '_label', + 'description' => $this->randomName() . '_description', + 'weight' => mt_rand(0, 127), + 'settings' => array( + 'test_instance_setting' => $this->randomName(), + ), + 'widget' => array( + 'type' => 'test_field_widget', + 'label' => 'Test Field', + 'settings' => array( + 'test_widget_setting' => $this->randomName(), + ), + ), + ); + field_create_instance($this->instance); + + for ($i = 0; $i < 3; ++$i) { + locale_inc_callback('locale_add_language', 'l' . $i, $this->randomString(), $this->randomString()); + } + } + + /** + * Ensure that only valid values are returned by field_multilingual_available_languages(). + */ + function testFieldAvailableLanguages() { + // Test 'translatable' fieldable info. + $field = $this->field; + $field['field_name'] .= '_untranslatable'; + $langcode = language_default(); + $suggested_languages = array($langcode->language); + $available_languages = field_multilingual_available_languages($this->obj_type, $field); + $this->assertTrue(count($available_languages) == 1 && $available_languages[0] === FIELD_LANGUAGE_NONE, t('Untranslatable entity: suggested language ignored.')); + + // Enable field translations for the entity. + field_test_fieldable_info_translatable('test_entity', TRUE); + + // Test hook_field_languages() invocation on a translatable field. + $this->field['settings']['test_hook_in'] = TRUE; + $enabled_languages = array_keys(language_list()); + $available_languages = field_multilingual_available_languages($this->obj_type, $this->field); + $this->assertTrue(in_array(FIELD_LANGUAGE_NONE, $available_languages), t('%language is an available language.', array('%language' => FIELD_LANGUAGE_NONE))); + foreach ($available_languages as $delta => $langcode) { + if ($langcode != FIELD_LANGUAGE_NONE) { + $this->assertTrue(in_array($langcode, $enabled_languages), t('%language is an enabled language.', array('%language' => $langcode))); + } + } + $this->assertFalse(in_array('xx', $available_languages), t('No invalid language was made available.')); + $this->assertTrue(count($available_languages) == count($enabled_languages), t('An enabled language was successfully made unavailable.')); + + // Test field_multilingual_available_languages() behavior for untranslatable fields. + $this->field['translatable'] = FALSE; + $this->field_name = $this->field['field_name'] = $this->instance['field_name'] = drupal_strtolower($this->randomName() . '_field_name'); + $available_languages = field_multilingual_available_languages($this->obj_type, $this->field); + $this->assertTrue(count($available_languages) == 1 && $available_languages[0] === FIELD_LANGUAGE_NONE, t('For untranslatable fields only neutral language is available.')); + + // Test language suggestions. + $this->field['settings']['test_hook_in'] = FALSE; + $this->field['translatable'] = TRUE; + $this->field_name = $this->field['field_name'] = $this->instance['field_name'] = drupal_strtolower($this->randomName() . '_field_name'); + $suggested_languages = array(); + $lang_count = mt_rand(1, count($enabled_languages) - 1); + for ($i = 0; $i < $lang_count; ++$i) { + do { + $langcode = $enabled_languages[mt_rand(0, $lang_count)]; + } + while (in_array($langcode, $suggested_languages)); + $suggested_languages[] = $langcode; + } + + $available_languages = field_multilingual_available_languages($this->obj_type, $this->field, $suggested_languages); + $this->assertEqual(count($available_languages), count($suggested_languages), t('Suggested languages were successfully made available.')); + foreach ($available_languages as $langcode) { + $this->assertTrue(in_array($langcode, $available_languages), t('Suggested language %language is available.', array('%language' => $langcode))); + } + + $this->field_name = $this->field['field_name'] = $this->instance['field_name'] = drupal_strtolower($this->randomName() . '_field_name'); + $suggested_languages = array('xx'); + $available_languages = field_multilingual_available_languages($this->obj_type, $this->field, $suggested_languages); + $this->assertTrue(empty($available_languages), t('An invalid suggested language was not made available.')); + } + + /** + * Test the multilanguage logic of _field_invoke(). + */ + function testFieldInvoke() { + $entity_type = 'test_entity'; + $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + + // Populate some extra languages to check if _field_invoke() correctly uses + // the result of field_multilingual_available_languages(). + $values = array(); + $extra_languages = mt_rand(1, 4); + $languages = $available_languages = field_multilingual_available_languages($this->obj_type, $this->field); + for ($i = 0; $i < $extra_languages; ++$i) { + $languages[] = $this->randomString(2); + } + + // For each given language provide some random values. + foreach ($languages as $langcode) { + for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { + $values[$langcode][$delta]['value'] = mt_rand(1, 127); + } + } + $entity->{$this->field_name} = $values; + + $results = _field_invoke('test_op', $entity_type, $entity); + foreach ($results as $langcode => $result) { + $hash = md5(serialize(array($entity_type, $entity, $this->field_name, $langcode, $values[$langcode]))); + // Check whether 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' => $langcode))); + } + $this->assertEqual(count($results), count($available_languages), t('No unavailable language has been processed.')); + } + + /** + * Test the multilanguage logic of _field_invoke_multiple(). + */ + function testFieldInvokeMultiple() { + $values = array(); + $entities = array(); + $entity_type = 'test_entity'; + $entity_count = mt_rand(1, 5); + $available_languages = field_multilingual_available_languages($this->obj_type, $this->field); + + for ($id = 1; $id <= $entity_count; ++$id) { + $entity = field_test_create_stub_entity($id, $id, $this->instance['bundle']); + $languages = $available_languages; + + // Populate some extra languages to check whether _field_invoke() + // correctly uses the result of field_multilingual_available_languages(). + $extra_languages = mt_rand(1, 4); + for ($i = 0; $i < $extra_languages; ++$i) { + $languages[] = $this->randomString(2); + } + + // For each given language provide some random values. + foreach ($languages as $langcode) { + for ($delta = 0; $delta < $this->field['cardinality']; $delta++) { + $values[$id][$langcode][$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 $langcode => $result) { + $hash = md5(serialize(array($entity_type, $entities[$id], $this->field_name, $langcode, $values[$id][$langcode]))); + // Check whether 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' => $langcode))); + } + $this->assertEqual(count($results), count($available_languages), t('No unavailable language has been processed for object %id.', array('%id' => $id))); + } + } + + /** + * Test translatable fields storage/retrieval. + */ + function testTranslatableFieldSaveLoad() { + // Enable field translations for nodes. + field_test_fieldable_info_translatable('node', TRUE); + $obj_info = field_info_fieldable_types('node'); + $this->assertTrue(count($obj_info['translation_handlers']), t('Nodes are translatable.')); + + // Prepare the field translations. + $eid = $evid = 1; + $obj_type = 'test_entity'; + $object = field_test_create_stub_entity($eid, $evid, $this->instance['bundle']); + $field_translations = array(); + foreach (field_multilingual_available_languages($obj_type, $this->field) as $langcode) { + $field_translations[$langcode] = FieldAttachTestCase::_generateTestFieldValues($this->field['cardinality']); + } + + // Save and reload the field translations. + $object->{$this->field_name} = $field_translations; + field_attach_insert($obj_type, $object); + unset($object->{$this->field_name}); + field_attach_load($obj_type, array($eid => $object)); + + // Check if the correct values were saved/loaded. + foreach ($field_translations as $langcode => $items) { + $result = TRUE; + foreach ($items as $delta => $item) { + $result = $result && $item['value'] == $object->{$this->field_name}[$langcode][$delta]['value']; + } + $this->assertTrue($result, t('%language translation correctly handled.', array('%language' => $langcode))); + } + } +} + +/** * Unit test class for field bulk delete and batch purge functionality. */ class FieldBulkDeleteTestCase extends DrupalWebTestCase { @@ -1898,7 +2159,7 @@ class FieldBulkDeleteTestCase extends Dr for ($i = 0; $i < 10; $i++) { $entity = field_test_create_stub_entity($id, $id, $bundle); foreach ($this->fields as $field) { - $entity->{$field['field_name']} = $this->_generateTestFieldValues($field['cardinality']); + $entity->{$field['field_name']}[FIELD_LANGUAGE_NONE] = $this->_generateTestFieldValues($field['cardinality']); } $this->entities[$id] = $entity; field_attach_insert($this->entity_type, $entity); 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.18 diff -u -p -r1.18 field_sql_storage.module --- modules/field/modules/field_sql_storage/field_sql_storage.module 11 Aug 2009 14:59:40 -0000 1.18 +++ modules/field/modules/field_sql_storage/field_sql_storage.module 21 Aug 2009 13:32:07 -0000 @@ -144,8 +144,16 @@ function _field_sql_storage_schema($fiel 'not null' => TRUE, 'description' => 'The sequence number for this data item, used for multi-value fields', ), + // @todo Consider storing language as integer. + '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 @@ function _field_sql_storage_schema($fiel $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, @@ -224,7 +232,7 @@ function field_sql_storage_field_storage if (!isset($skip_fields[$instance['field_id']]) && (!isset($options['field_id']) || $options['field_id'] == $instance['field_id'])) { $objects[$id]->{$field_name} = array(); $field_ids[$instance['field_id']][] = $load_current ? $id : $vid; - $delta_count[$id][$field_name] = 0; + $delta_count[$id][$field_name] = array(); } } } @@ -238,6 +246,7 @@ function field_sql_storage_field_storage ->fields('t') ->condition('etid', $etid) ->condition($load_current ? 'entity_id' : 'revision_id', $ids, 'IN') + ->condition('language', field_multilingual_available_languages($obj_type, $field), 'IN') ->orderBy('delta'); if (empty($options['deleted'])) { @@ -247,7 +256,11 @@ function field_sql_storage_field_storage $results = $query->execute(); foreach ($results as $row) { - if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta_count[$row->entity_id][$field_name] < $field['cardinality']) { + if (!isset($delta_count[$row->entity_id][$field_name][$row->language])) { + $delta_count[$row->entity_id][$field_name][$row->language] = 0; + } + + if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta_count[$row->entity_id][$field_name][$row->language] < $field['cardinality']) { $item = array(); // For each column declared by the field, populate the item // from the prefixed database column. @@ -257,8 +270,8 @@ function field_sql_storage_field_storage } // 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]++; } } } @@ -288,17 +301,33 @@ function field_sql_storage_field_storage // Function property_exists() is slower, so we catch the more frequent cases // where it's an empty array with the faster isset(). if (isset($object->$field_name) || property_exists($object, $field_name)) { + $available_languages = field_multilingual_available_languages($obj_type, $field); + $available_translations = is_array($object->$field_name) ? array_intersect($available_languages, array_keys($object->$field_name)) : FALSE; + // Delete and insert, rather than update, in case a value was added. - if ($op == FIELD_STORAGE_UPDATE) { - db_delete($table_name)->condition('etid', $etid)->condition('entity_id', $id)->execute(); + // If no translation is available, empty the field for all the available languages. + if ($op == FIELD_STORAGE_UPDATE && count($available_translations)) { + $languages = empty($object->$field_name) ? $available_languages : $available_translations; + + db_delete($table_name) + ->condition('etid', $etid) + ->condition('entity_id', $id) + ->condition('language', $languages, 'IN') + ->execute(); + if (isset($vid)) { - db_delete($revision_name)->condition('etid', $etid)->condition('entity_id', $id)->condition('revision_id', $vid)->execute(); + db_delete($revision_name) + ->condition('etid', $etid) + ->condition('entity_id', $id) + ->condition('revision_id', $vid) + ->condition('language', $languages, 'IN') + ->execute(); } } - if ($object->$field_name) { + if (!empty($available_translations)) { // Prepare the multi-insert query. - $columns = array('etid', 'entity_id', 'revision_id', 'bundle', 'delta'); + $columns = array('etid', 'entity_id', 'revision_id', 'bundle', 'delta', 'language'); foreach ($field['columns'] as $column => $attributes) { $columns[] = _field_sql_storage_columnname($field_name, $column); } @@ -307,25 +336,30 @@ function field_sql_storage_field_storage $revision_query = db_insert($revision_name)->fields($columns); } - $delta_count = 0; - foreach ($object->$field_name as $delta => $item) { - $record = array( - 'etid' => $etid, - 'entity_id' => $id, - 'revision_id' => $vid, - 'bundle' => $bundle, - 'delta' => $delta, - ); - foreach ($field['columns'] as $column => $attributes) { - $record[_field_sql_storage_columnname($field_name, $column)] = isset($item[$column]) ? $item[$column] : NULL; - } - $query->values($record); - if (isset($vid)) { - $revision_query->values($record); - } - - if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED && ++$delta_count == $field['cardinality']) { - break; + foreach ($available_translations as $langcode) { + if ($items = $object->{$field_name}[$langcode]) { + $delta_count = 0; + foreach ($items as $delta => $item) { + $record = array( + 'etid' => $etid, + 'entity_id' => $id, + 'revision_id' => $vid, + 'bundle' => $bundle, + 'delta' => $delta, + 'language' => $langcode, + ); + foreach ($field['columns'] as $column => $attributes) { + $record[_field_sql_storage_columnname($field_name, $column)] = isset($item[$column]) ? $item[$column] : NULL; + } + $query->values($record); + if (isset($vid)) { + $revision_query->values($record); + } + + if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED && ++$delta_count == $field['cardinality']) { + break; + } + } } } Index: modules/field/modules/field_sql_storage/field_sql_storage.test =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/field_sql_storage/field_sql_storage.test,v retrieving revision 1.6 diff -u -p -r1.6 field_sql_storage.test --- modules/field/modules/field_sql_storage/field_sql_storage.test 28 Jul 2009 19:18:06 -0000 1.6 +++ modules/field/modules/field_sql_storage/field_sql_storage.test 21 Aug 2009 13:28:40 -0000 @@ -56,9 +56,10 @@ class FieldSqlStorageTestCase extends Dr function testFieldAttachLoad() { $entity_type = 'test_entity'; $eid = 0; + $langcode = FIELD_LANGUAGE_NONE; $etid = _field_sql_storage_etid($entity_type); - $columns = array('etid', 'entity_id', 'revision_id', 'delta', $this->field_name . '_value'); + $columns = array('etid', 'entity_id', 'revision_id', 'delta', 'language', $this->field_name . '_value'); // Insert data for four revisions to the field revisions table $query = db_insert($this->revision_table)->fields($columns); @@ -68,7 +69,7 @@ class FieldSqlStorageTestCase extends Dr 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, $langcode, $value)); } } $query->execute(); @@ -76,7 +77,7 @@ class FieldSqlStorageTestCase extends Dr // 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, $langcode, $value)); } $query->execute(); @@ -85,10 +86,10 @@ class FieldSqlStorageTestCase extends Dr 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}[$langcode][$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}[$langcode]), "No extraneous value gets loaded for current revision."); } } @@ -98,13 +99,23 @@ class FieldSqlStorageTestCase extends Dr 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}[$langcode][$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}[$langcode]), "No extraneous value gets loaded for revision $evid."); } } } + + // Add a translation in an unavailable language and verify it is not loaded. + $eid = $evid = 1; + $unavailable_language = 'xx'; + $entity = field_test_create_stub_entity($eid, $evid, $this->instance['bundle']); + $values = array($etid, $eid, $evid, 0, $unavailable_language, mt_rand(1, 127)); + db_insert($this->table)->fields($columns)->values($values)->execute(); + db_insert($this->revision_table)->fields($columns)->values($values)->execute(); + field_attach_load($entity_type, array($eid => $entity)); + $this->assertFalse(array_key_exists($unavailable_language, $entity->{$this->field_name}), 'Field translation in an unavailable language ignored'); } /** @@ -114,6 +125,7 @@ class FieldSqlStorageTestCase extends Dr function testFieldAttachInsertAndUpdate() { $entity_type = 'test_entity'; $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $langcode = FIELD_LANGUAGE_NONE; // Test insert. $values = array(); @@ -122,7 +134,7 @@ class FieldSqlStorageTestCase extends Dr 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}[$langcode] = $rev_values[0] = $values; field_attach_insert($entity_type, $entity); $rows = db_select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', PDO::FETCH_ASSOC); @@ -142,7 +154,7 @@ class FieldSqlStorageTestCase extends Dr 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}[$langcode] = $rev_values[1] = $values; field_attach_update($entity_type, $entity); $rows = db_select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', PDO::FETCH_ASSOC); foreach ($values as $delta => $value) { @@ -170,9 +182,9 @@ class FieldSqlStorageTestCase extends Dr } $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}[$langcode]); field_attach_update($entity_type, $entity); $rows = db_select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', PDO::FETCH_ASSOC); foreach ($values as $delta => $value) { @@ -182,7 +194,7 @@ class FieldSqlStorageTestCase extends Dr } // Check that update with an empty $object->$field_name empties the field. - $entity->{$this->field_name} = NULL; + $entity->{$this->field_name}[$langcode] = NULL; field_attach_update($entity_type, $entity); $rows = db_select($this->table, 't')->fields('t')->execute()->fetchAllAssoc('delta', PDO::FETCH_ASSOC); $this->assertEqual(count($rows), 0, t("Update with an empty field_name entry empties the field.")); @@ -194,6 +206,7 @@ class FieldSqlStorageTestCase extends Dr function testFieldAttachSaveMissingData() { $entity_type = 'test_entity'; $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); + $langcode = FIELD_LANGUAGE_NONE; // Insert: Field is missing field_attach_insert($entity_type, $entity); @@ -204,7 +217,7 @@ class FieldSqlStorageTestCase extends Dr $this->assertEqual($count, 0, 'Missing field results in no inserts'); // Insert: Field is NULL - $entity->{$this->field_name} = NULL; + $entity->{$this->field_name}[$langcode] = NULL; field_attach_insert($entity_type, $entity); $count = db_select($this->table) ->countQuery() @@ -213,7 +226,7 @@ class FieldSqlStorageTestCase extends Dr $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}[$langcode] = array(0 => array('value' => 1)); field_attach_insert($entity_type, $entity); $count = db_select($this->table) ->countQuery() @@ -238,5 +251,47 @@ class FieldSqlStorageTestCase extends Dr ->execute() ->fetchField(); $this->assertEqual($count, 0, 'NULL field leaves no data in table'); + + // Add a translation in an unavailable language. + $unavailable_language = 'xx'; + db_insert($this->table) + ->fields(array('etid', 'bundle', 'deleted', 'entity_id', 'revision_id', 'delta', 'language')) + ->values(array(_field_sql_storage_etid($entity_type), $this->instance['bundle'], 0, 0, 0, 0, $unavailable_language)) + ->execute(); + $count = db_select($this->table) + ->countQuery() + ->execute() + ->fetchField(); + $this->assertEqual($count, 1, 'Field translation in an unavailable language saved.'); + + // Again add some real data. + $entity->{$this->field_name}[$langcode] = array(0 => array('value' => 1)); + field_attach_insert($entity_type, $entity); + $count = db_select($this->table) + ->countQuery() + ->execute() + ->fetchField(); + $this->assertEqual($count, 2, 'Field data saved.'); + + // Update: Field translation is missing but field is not empty. Translation + // data should survive. + $entity->{$this->field_name}[$unavailable_language] = array(mt_rand(1, 127)); + unset($entity->{$this->field_name}[$langcode]); + field_attach_update($entity_type, $entity); + $count = db_select($this->table) + ->countQuery() + ->execute() + ->fetchField(); + $this->assertEqual($count, 2, 'Missing field translation leaves data in table.'); + + // Update: Field translation is NULL but field is not empty. Translation + // data should be wiped. + $entity->{$this->field_name}[$langcode] = NULL; + field_attach_update($entity_type, $entity); + $count = db_select($this->table) + ->countQuery() + ->execute() + ->fetchField(); + $this->assertEqual($count, 1, 'NULL field translation is wiped.'); } } Index: modules/field/modules/list/list.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/list/list.module,v retrieving revision 1.8 diff -u -p -r1.8 list.module --- modules/field/modules/list/list.module 19 Aug 2009 13:31:13 -0000 1.8 +++ modules/field/modules/list/list.module 21 Aug 2009 13:28:40 -0000 @@ -228,12 +228,12 @@ function list_allowed_values_validate($e * 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, $langcode, $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']][$langcode][$delta][] = array( 'error' => 'list_illegal_value', 'message' => t('%name: illegal value.', array('%name' => t($instance['label']))), ); Index: modules/field/modules/number/number.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/number/number.module,v retrieving revision 1.12 diff -u -p -r1.12 number.module --- modules/field/modules/number/number.module 19 Aug 2009 13:31:13 -0000 1.12 +++ modules/field/modules/number/number.module 21 Aug 2009 13:28:40 -0000 @@ -170,17 +170,17 @@ function number_field_instance_settings_ * - '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, $langcode, $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']][$langcode][$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']][$langcode][$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/text/text.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/text/text.module,v retrieving revision 1.19 diff -u -p -r1.19 text.module --- modules/field/modules/text/text.module 20 Aug 2009 10:56:33 -0000 1.19 +++ modules/field/modules/text/text.module 21 Aug 2009 13:28:41 -0000 @@ -180,7 +180,7 @@ function text_field_instance_settings_fo * - '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, $langcode, $items, &$errors) { foreach ($items as $delta => $item) { foreach (array('value' => t('full text'), 'summary' => t('summary')) as $column => $desc) { if (!empty($item[$column])) { @@ -193,7 +193,7 @@ function text_field_validate($obj_type, $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']][$langcode][$delta][] = array( 'error' => "text_{$column}_length", 'message' => $message, ); @@ -211,9 +211,7 @@ function text_field_validate($obj_type, * 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, $langcode, &$items) { foreach ($objects as $id => $object) { foreach ($items[$id] as $delta => $item) { if (!empty($instances[$id]['settings']['text_processing'])) { @@ -221,10 +219,9 @@ function text_field_load($obj_type, $obj // handled by text_field_sanitize(). $format = $item['format']; if (filter_format_allowcache($format)) { - $lang = isset($object->language) ? $object->language : $language->language; - $items[$id][$delta]['safe'] = isset($item['value']) ? check_markup($item['value'], $format, $lang, FALSE) : ''; + $items[$id][$delta]['safe'] = isset($item['value']) ? check_markup($item['value'], $format, $langcode, FALSE) : ''; if ($field['type'] == 'text_with_summary') { - $items[$id][$delta]['safe_summary'] = isset($item['summary']) ? check_markup($item['summary'], $format, $lang, FALSE) : ''; + $items[$id][$delta]['safe_summary'] = isset($item['summary']) ? check_markup($item['summary'], $format, $langcode, FALSE) : ''; } } } @@ -243,8 +240,7 @@ function text_field_load($obj_type, $obj * * @see text_field_load() */ -function text_field_sanitize($obj_type, $object, $field, $instance, &$items) { - global $language; +function text_field_sanitize($obj_type, $object, $field, $instance, $langcode, &$items) { foreach ($items as $delta => $item) { // Only sanitize items which were not already processed inside // text_field_load(), i.e. items with uncacheable text formats, or coming @@ -252,10 +248,9 @@ function text_field_sanitize($obj_type, if (!isset($items[$delta]['safe'])) { if (!empty($instance['settings']['text_processing'])) { $format = $item['format']; - $lang = isset($object->language) ? $object->language : $language->language; - $items[$delta]['safe'] = isset($item['value']) ? check_markup($item['value'], $format, $lang) : ''; + $items[$delta]['safe'] = isset($item['value']) ? check_markup($item['value'], $format, $langcode) : ''; if ($field['type'] == 'text_with_summary') { - $items[$delta]['safe_summary'] = isset($item['summary']) ? check_markup($item['summary'], $format, $lang) : ''; + $items[$delta]['safe_summary'] = isset($item['summary']) ? check_markup($item['summary'], $format, $langcode) : ''; } } else { Index: modules/field/modules/text/text.test =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/text/text.test,v retrieving revision 1.9 diff -u -p -r1.9 text.test --- modules/field/modules/text/text.test 27 Jul 2009 20:15:35 -0000 1.9 +++ modules/field/modules/text/text.test 21 Aug 2009 13:28:41 -0000 @@ -50,8 +50,9 @@ class TextFieldTestCase extends DrupalWe 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); + $langcode = FIELD_LANGUAGE_NONE; for ($i = 0; $i <= $max_length + 2; $i++) { - $entity->{$this->field['field_name']}[0]['value'] = str_repeat('x', $i); + $entity->{$this->field['field_name']}[$langcode][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 @@ class TextFieldTestCase extends DrupalWe ) ); field_create_instance($this->instance); + $langcode = FIELD_LANGUAGE_NONE; // Display creation form. $this->drupalGet('test-entity/add/test-bundle'); - $this->assertFieldByName($this->field_name . '[0][value]', '', t('Widget is displayed')); - $this->assertNoFieldByName($this->field_name . '[0][format]', '1', t('Format selector is not displayed')); + $this->assertFieldByName("{$this->field_name}[$langcode][0][value]", '', t('Widget is displayed')); + $this->assertNoFieldByName("{$this->field_name}[$langcode][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}[$langcode][0][value]" => $value, ); $this->drupalPost(NULL, $edit, t('Save')); preg_match('|test-entity/(\d+)/edit|', $this->url, $match); @@ -143,18 +145,19 @@ class TextFieldTestCase extends DrupalWe ) ); field_create_instance($this->instance); + $langcode = FIELD_LANGUAGE_NONE; // Display creation form. // By default, the user only has access to 'Filtered HTML', and no format // selector is displayed $this->drupalGet('test-entity/add/test-bundle'); - $this->assertFieldByName($this->field_name . '[0][value]', '', t('Widget is displayed')); - $this->assertNoFieldByName($this->field_name . '[0][value_format]', '1', t('Format selector is not displayed')); + $this->assertFieldByName("{$this->field_name}[$langcode][0][value]", '', t('Widget is displayed')); + $this->assertNoFieldByName("{$this->field_name}[$langcode][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}[$langcode][0][value]" => $value, ); $this->drupalPost(NULL, $edit, t('Save')); preg_match('|test-entity/(\d+)/edit|', $this->url, $match); @@ -174,12 +177,12 @@ class TextFieldTestCase extends DrupalWe // 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}[$langcode][0][value]", '', t('Widget is displayed')); + $this->assertFieldByName("{$this->field_name}[$langcode][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}[$langcode][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/theme/field.tpl.php =================================================================== RCS file: /cvs/drupal/drupal/modules/field/theme/field.tpl.php,v retrieving revision 1.3 diff -u -p -r1.3 field.tpl.php --- modules/field/theme/field.tpl.php 22 Jun 2009 09:10:04 -0000 1.3 +++ modules/field/theme/field.tpl.php 21 Aug 2009 13:28:41 -0000 @@ -18,6 +18,8 @@ * - $label: The item label. * - $label_display: Position of label display, inline, above, or hidden. * - $field_empty: Whether the field has any valid value. + * - $field_language: The field language. + * - $field_translatable: Whether the field is translatable or not. * * Each $item in $items contains: * - 'view' - the themed view for that item Index: modules/field_ui/field_ui.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field_ui/field_ui.admin.inc,v retrieving revision 1.2 diff -u -p -r1.2 field_ui.admin.inc --- modules/field_ui/field_ui.admin.inc 21 Aug 2009 02:54:46 -0000 1.2 +++ modules/field_ui/field_ui.admin.inc 21 Aug 2009 13:28:41 -0000 @@ -1207,7 +1207,7 @@ function field_ui_default_value_widget($ ); drupal_function_exists('field_default_form'); // @todo Allow multiple values (requires more work on 'add more' JS handler). - $widget_form = field_default_form(NULL, NULL, $field, $instance, $items, $form, $form_state, 0); + $widget_form = field_default_form(NULL, NULL, $field, $instance, FIELD_LANGUAGE_NONE, $items, $form, $form_state, 0); $form['instance']['default_value_widget'] += $widget_form; $form['#fields'][$field['field_name']]['form_path'] = array( 'instance', Index: modules/filter/filter.test =================================================================== RCS file: /cvs/drupal/drupal/modules/filter/filter.test,v retrieving revision 1.30 diff -u -p -r1.30 filter.test --- modules/filter/filter.test 15 Aug 2009 06:45:31 -0000 1.30 +++ modules/filter/filter.test 21 Aug 2009 13:28:41 -0000 @@ -110,8 +110,9 @@ class FilterAdminTestCase extends Drupal $edit = array(); $edit['title'] = $this->randomName(); - $edit['body[0][value]'] = $body . '' . $extra_text . ''; - $edit['body[0][value_format]'] = $filtered; + $langcode = FIELD_LANGUAGE_NONE; + $edit["body[$langcode][0][value]"] = $body . '' . $extra_text . ''; + $edit["body[$langcode][0][value_format]"] = $filtered; $this->drupalPost('node/add/page', $edit, t('Save')); $this->assertRaw(t('Page %title has been created.', array('%title' => $edit['title'])), t('Filtered node created.')); Index: modules/forum/forum.test =================================================================== RCS file: /cvs/drupal/drupal/modules/forum/forum.test,v retrieving revision 1.27 diff -u -p -r1.27 forum.test --- modules/forum/forum.test 3 Aug 2009 03:04:33 -0000 1.27 +++ modules/forum/forum.test 21 Aug 2009 13:28:41 -0000 @@ -232,9 +232,10 @@ class ForumTestCase extends DrupalWebTes $body = $this->randomName(200); $tid = $forum['tid']; // Without this being set, post variable equals the first non-blank in select items list. + $langcode = FIELD_LANGUAGE_NONE; $edit = array( 'title' => $title, - 'body[0][value]' => $body, + "body[$langcode][0][value]" => $body, 'taxonomy[1]' => $tid ); @@ -321,7 +322,8 @@ class ForumTestCase extends DrupalWebTes // Edit forum node (including moving it to another forum). $edit = array(); $edit['title'] = 'node/' . $node->nid; - $edit['body[0][value]'] = $this->randomName(256); + $langcode = FIELD_LANGUAGE_NONE; + $edit["body[$langcode][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.37 diff -u -p -r1.37 locale.test --- modules/locale/locale.test 18 Aug 2009 11:22:36 -0000 1.37 +++ modules/locale/locale.test 21 Aug 2009 13:28:41 -0000 @@ -1389,7 +1389,7 @@ class LocaleContentFunctionalTest extend $edit = array( 'type' => 'page', 'title' => $node_title, - 'body' => array(array('value' => $node_body)), + 'body' => array(FIELD_LANGUAGE_NONE => array(array('value' => $node_body))), 'language' => $langcode, ); $node = $this->drupalCreateNode($edit); @@ -1483,6 +1483,7 @@ class UILanguageNegotiationTest extends // into database when seen by t(). Without doing this, our target string // is for some reason not found when doing translate search. This might // be some bug. + drupal_static_reset('language_list'); $languages = language_list('enabled'); variable_set('language_default', $languages[1]['vi']); // First visit this page to make sure our target string is searchable. Index: modules/node/node.install =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.install,v retrieving revision 1.27 diff -u -p -r1.27 node.install --- modules/node/node.install 19 Aug 2009 22:46:05 -0000 1.27 +++ modules/node/node.install 21 Aug 2009 13:28:41 -0000 @@ -511,15 +511,15 @@ function node_update_7006(&$context) { 'type' => $revision->type, ); if (!empty($revision->teaser) && $revision->teaser != text_summary($revision->body)) { - $node->body[0]['summary'] = $revision->teaser; + $node->body[FIELD_LANGUAGE_NONE][0]['summary'] = $revision->teaser; } // Do this after text_summary() above. $break = ''; if (substr($revision->body, 0, strlen($break)) == $break) { $revision->body = substr($revision->body, strlen($break)); } - $node->body[0]['value'] = $revision->body; - $node->body[0]['format'] = $revision->format; + $node->body[FIELD_LANGUAGE_NONE][0]['value'] = $revision->body; + $node->body[FIELD_LANGUAGE_NONE][0]['format'] = $revision->format; // This is a core update and no contrib modules are enabled yet, so // we can assume default field storage for a faster update. field_sql_storage_field_storage_write('node', $node, FIELD_STORAGE_INSERT, array()); Index: modules/node/node.pages.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.pages.inc,v retrieving revision 1.75 diff -u -p -r1.75 node.pages.inc --- modules/node/node.pages.inc 19 Aug 2009 13:31:13 -0000 1.75 +++ modules/node/node.pages.inc 21 Aug 2009 13:28:41 -0000 @@ -285,7 +285,7 @@ function node_form(&$form_state, $node) $form['#theme'] = array($node->type . '_node_form', 'node_form'); $form['#builder_function'] = 'node_form_submit_build_node'; - field_attach_form('node', $node, $form, $form_state); + field_attach_form('node', $node, $form, $form_state, $node->language); return $form; } Index: modules/node/node.test =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.test,v retrieving revision 1.38 diff -u -p -r1.38 node.test --- modules/node/node.test 28 Jul 2009 19:18:06 -0000 1.38 +++ modules/node/node.test 21 Aug 2009 13:28:41 -0000 @@ -141,7 +141,7 @@ class NodeRevisionsTestCase extends Drup // Confirm the correct revision text appears on "view revisions" page. $this->drupalGet("node/$node->nid/revisions/$node->vid/view"); - $this->assertText($node->body[0]['value'], t('Correct text displays for version.')); + $this->assertText($node->body[FIELD_LANGUAGE_NONE][0]['value'], t('Correct text displays for version.')); // Confirm the correct log message appears on "revisions overview" page. $this->drupalGet("node/$node->nid/revisions"); @@ -155,7 +155,7 @@ class NodeRevisionsTestCase extends Drup array('@type' => 'Page', '%title' => $nodes[1]->title, '%revision-date' => format_date($nodes[1]->revision_timestamp))), t('Revision reverted.')); $reverted_node = node_load($node->nid); - $this->assertTrue(($nodes[1]->body[0]['value'] == $reverted_node->body[0]['value']), t('Node reverted correctly.')); + $this->assertTrue(($nodes[1]->body[FIELD_LANGUAGE_NONE][0]['value'] == $reverted_node->body[FIELD_LANGUAGE_NONE][0]['value']), t('Node reverted correctly.')); // Confirm revisions delete properly. $this->drupalPost("node/$node->nid/revisions/{$nodes[1]->vid}/delete", array(), t('Delete')); @@ -186,7 +186,8 @@ class PageEditTestCase extends DrupalWeb * Check node edit functionality. */ function testPageEdit() { - $body_key = 'body[0][value]'; + $langcode = FIELD_LANGUAGE_NONE; + $body_key = "body[$langcode][0][value]"; // Create node to edit. $edit = array(); $edit['title'] = $this->randomName(8); @@ -241,7 +242,8 @@ class PagePreviewTestCase extends Drupal * Check the node preview functionality. */ function testPagePreview() { - $body_key = 'body[0][value]'; + $langcode = FIELD_LANGUAGE_NONE; + $body_key = "body[$langcode][0][value]"; // Fill in node creation form and preview node. $edit = array(); @@ -263,7 +265,8 @@ class PagePreviewTestCase extends Drupal * Check the node preview functionality, when using revisions. */ function testPagePreviewWithRevisions() { - $body_key = 'body[0][value]'; + $langcode = FIELD_LANGUAGE_NONE; + $body_key = "body[$langcode][0][value]"; // Force revision on page content. variable_set('node_options_page', array('status', 'revision')); @@ -311,7 +314,8 @@ class PageCreationTestCase extends Drupa // Create a node. $edit = array(); $edit['title'] = $this->randomName(8); - $edit['body[0][value]'] = $this->randomName(16); + $langcode = FIELD_LANGUAGE_NONE; + $edit["body[$langcode][0][value]"] = $this->randomName(16); $this->drupalPost('node/add/page', $edit, t('Save')); // Check that the page has been created. @@ -377,7 +381,7 @@ class SummaryLengthTestCase extends Drup function testSummaryLength() { // Create a node to view. $settings = array( - 'body' => array(array('value' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae arcu at leo cursus laoreet. Curabitur dui tortor, adipiscing malesuada tempor in, bibendum ac diam. Cras non tellus a libero pellentesque condimentum. What is a Drupalism? Suspendisse ac lacus libero. Ut non est vel nisl faucibus interdum nec sed leo. Pellentesque sem risus, vulputate eu semper eget, auctor in libero. Ut fermentum est vitae metus convallis scelerisque. Phasellus pellentesque rhoncus tellus, eu dignissim purus posuere id. Quisque eu fringilla ligula. Morbi ullamcorper, lorem et mattis egestas, tortor neque pretium velit, eget eleifend odio turpis eu purus. Donec vitae metus quis leo pretium tincidunt a pulvinar sem. Morbi adipiscing laoreet mauris vel placerat. Nullam elementum, nisl sit amet scelerisque malesuada, dolor nunc hendrerit quam, eu ultrices erat est in orci. Curabitur feugiat egestas nisl sed accumsan.')), + 'body' => array(FIELD_LANGUAGE_NONE => array(array('value' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae arcu at leo cursus laoreet. Curabitur dui tortor, adipiscing malesuada tempor in, bibendum ac diam. Cras non tellus a libero pellentesque condimentum. What is a Drupalism? Suspendisse ac lacus libero. Ut non est vel nisl faucibus interdum nec sed leo. Pellentesque sem risus, vulputate eu semper eget, auctor in libero. Ut fermentum est vitae metus convallis scelerisque. Phasellus pellentesque rhoncus tellus, eu dignissim purus posuere id. Quisque eu fringilla ligula. Morbi ullamcorper, lorem et mattis egestas, tortor neque pretium velit, eget eleifend odio turpis eu purus. Donec vitae metus quis leo pretium tincidunt a pulvinar sem. Morbi adipiscing laoreet mauris vel placerat. Nullam elementum, nisl sit amet scelerisque malesuada, dolor nunc hendrerit quam, eu ultrices erat est in orci. Curabitur feugiat egestas nisl sed accumsan.'))), 'promote' => 1, ); $node = $this->drupalCreateNode($settings); @@ -500,7 +504,8 @@ class NodePostSettingsTestCase extends D // Create a node. $edit = array(); $edit['title'] = $this->randomName(8); - $edit['body[0][value]'] = $this->randomName(16); + $langcode = FIELD_LANGUAGE_NONE; + $edit["body[$langcode][0][value]"] = $this->randomName(16); $this->drupalPost('node/add/page', $edit, t('Save')); // Check that the post information is displayed. @@ -521,7 +526,8 @@ class NodePostSettingsTestCase extends D // Create a node. $edit = array(); $edit['title'] = $this->randomName(8); - $edit['body[0][value]'] = $this->randomName(16); + $langcode = FIELD_LANGUAGE_NONE; + $edit["body[$langcode][0][value]"] = $this->randomName(16); $this->drupalPost('node/add/page', $edit, t('Save')); // Check that the post information is displayed. @@ -764,7 +770,7 @@ class NodeSaveTestCase extends DrupalWeb $title = $this->randomName(8); $node = array( 'title' => $title, - 'body' => array(array('value' => $this->randomName(32))), + 'body' => array(FIELD_LANGUAGE_NONE => array(array('value' => $this->randomName(32)))), 'uid' => $this->web_user->uid, 'type' => 'article', 'nid' => $test_nid, Index: modules/node/node.tpl.php =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.tpl.php,v retrieving revision 1.19 diff -u -p -r1.19 node.tpl.php --- modules/node/node.tpl.php 6 Aug 2009 05:05:59 -0000 1.19 +++ modules/node/node.tpl.php 21 Aug 2009 13:28:41 -0000 @@ -59,6 +59,13 @@ * - $logged_in: Flags true when the current user is a logged-in member. * - $is_admin: Flags true when the current user is an administrator. * + * Field variables: for each field instance attached to the node a corresponding + * variable is defined, e.g. $node->body becomes $body. When needing to access + * a field's raw values, developers/themers are strongly encouraged to use these + * variables. Otherwise they will have to explicitly specify the desired field + * language, e.g. $node->body['en'], thus overriding any language negotiation + * rule that was previously applied. + * * @see template_preprocess() * @see template_preprocess_node() * @see template_process() Index: modules/path/path.test =================================================================== RCS file: /cvs/drupal/drupal/modules/path/path.test,v retrieving revision 1.17 diff -u -p -r1.17 path.test --- modules/path/path.test 11 Aug 2009 11:52:46 -0000 1.17 +++ modules/path/path.test 21 Aug 2009 13:28:41 -0000 @@ -180,6 +180,7 @@ class PathLanguageTestCase extends Drupa $edit['langcode'] = 'fr'; $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); + drupal_static_reset('language_list'); // Set language negotiation to "Path prefix with fallback". variable_set('language_negotiation', LANGUAGE_NEGOTIATION_PATH); @@ -212,7 +213,8 @@ class PathLanguageTestCase extends Drupa $this->clickLink(t('add translation')); $edit = array(); $edit['title'] = $this->randomName(); - $edit['body[0][value]'] = $this->randomName(); + $langcode = FIELD_LANGUAGE_NONE; + $edit["body[$langcode][0][value]"] = $this->randomName(); $edit['path'] = $this->randomName(); $this->drupalPost(NULL, $edit, t('Save')); Index: modules/php/php.test =================================================================== RCS file: /cvs/drupal/drupal/modules/php/php.test,v retrieving revision 1.14 diff -u -p -r1.14 php.test --- modules/php/php.test 13 Jul 2009 21:51:11 -0000 1.14 +++ modules/php/php.test 21 Aug 2009 13:28:41 -0000 @@ -23,7 +23,7 @@ class PHPTestCase extends DrupalWebTestC * @return stdObject Node object. */ function createNodeWithCode() { - return $this->drupalCreateNode(array('body' => array(array('value' => '')))); + return $this->drupalCreateNode(array('body' => array(FIELD_LANGUAGE_NONE => array(array('value' => ''))))); } } @@ -60,7 +60,8 @@ class PHPFilterTestCase extends PHPTestC // Change filter to PHP filter and see that PHP code is evaluated. $edit = array(); - $edit['body[0][value_format]'] = 3; + $langcode = FIELD_LANGUAGE_NONE; + $edit["body[$langcode][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/search/search.test =================================================================== RCS file: /cvs/drupal/drupal/modules/search/search.test,v retrieving revision 1.27 diff -u -p -r1.27 search.test --- modules/search/search.test 31 Jul 2009 19:01:02 -0000 1.27 +++ modules/search/search.test 21 Aug 2009 13:28:41 -0000 @@ -312,7 +312,7 @@ class SearchRankingTestCase extends Drup // Create nodes for testing. foreach ($node_ranks as $node_rank) { - $settings = array('type' => 'page', 'title' => 'Drupal rocks', 'body' => array(array('value' => "Drupal's search rocks"))); + $settings = array('type' => 'page', 'title' => 'Drupal rocks', 'body' => array(FIELD_LANGUAGE_NONE => array(array('value' => "Drupal's search rocks")))); foreach (array(0, 1) as $num) { if ($num == 1) { switch ($node_rank) { @@ -321,7 +321,7 @@ class SearchRankingTestCase extends Drup $settings[$node_rank] = 1; break; case 'relevance': - $settings['body'][0]['value'] .= " really rocks"; + $settings['body'][FIELD_LANGUAGE_NONE][0]['value'] .= " really rocks"; break; case 'recent': $settings['created'] = REQUEST_TIME + 3600; Index: modules/simpletest/drupal_web_test_case.php =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/drupal_web_test_case.php,v retrieving revision 1.140 diff -u -p -r1.140 drupal_web_test_case.php --- modules/simpletest/drupal_web_test_case.php 21 Aug 2009 07:50:07 -0000 1.140 +++ modules/simpletest/drupal_web_test_case.php 21 Aug 2009 13:28:41 -0000 @@ -693,7 +693,7 @@ class DrupalWebTestCase extends DrupalTe protected function drupalCreateNode($settings = array()) { // Populate defaults array. $settings += array( - 'body' => array(array()), + 'body' => array(FIELD_LANGUAGE_NONE => array(array())), 'title' => $this->randomName(8), 'comment' => 2, 'changed' => REQUEST_TIME, @@ -730,7 +730,7 @@ class DrupalWebTestCase extends DrupalTe 'value' => $this->randomName(32), 'format' => FILTER_FORMAT_DEFAULT ); - $settings['body'][0] += $body; + $settings['body'][FIELD_LANGUAGE_NONE][0] += $body; $node = (object) $settings; node_save($node); Index: modules/simpletest/tests/common.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/common.test,v retrieving revision 1.64 diff -u -p -r1.64 common.test --- modules/simpletest/tests/common.test 17 Aug 2009 19:14:41 -0000 1.64 +++ modules/simpletest/tests/common.test 21 Aug 2009 13:28:41 -0000 @@ -374,7 +374,7 @@ class CascadingStylesheetsTestCase exten // Create a node, using the PHP filter that tests drupal_add_css(). $settings = array( 'type' => 'page', - 'body' => array(array('value' => t('This tests the inline CSS!') . "", 'format' => 3)), // PHP filter. + 'body' => array(FIELD_LANGUAGE_NONE => array(array('value' => t('This tests the inline CSS!') . "", 'format' => 3))), // PHP filter. 'promote' => 1, ); $node = $this->drupalCreateNode($settings); Index: modules/simpletest/tests/field_test.module =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/field_test.module,v retrieving revision 1.15 diff -u -p -r1.15 field_test.module --- modules/simpletest/tests/field_test.module 19 Aug 2009 22:46:05 -0000 1.15 +++ modules/simpletest/tests/field_test.module 21 Aug 2009 13:28:41 -0000 @@ -88,6 +88,15 @@ function field_test_fieldable_info() { } /** + * Implement hook_fieldable_info_alter(). + */ +function field_test_fieldable_info_alter(&$info) { + foreach (field_test_fieldable_info_translatable() as $obj_type => $translatable) { + $info[$obj_type]['translation_handlers']['field_test'] = TRUE; + } +} + +/** * Create a new bundle for test_entity objects. * * @param $bundle_name @@ -372,7 +381,7 @@ function field_test_field_schema($field) /** * 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, $langcode, &$items, $age) { foreach ($items as $id => $item) { // To keep the test non-intrusive, only act for instances with the // test_hook_field_load setting explicitly set to TRUE. @@ -393,10 +402,10 @@ function field_test_field_load($obj_type * 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, $langcode, $items, &$errors) { foreach ($items as $delta => $item) { if ($item['value'] == -1) { - $errors[$field['field_name']][$delta][] = array( + $errors[$field['field_name']][$langcode][$delta][] = array( 'error' => 'field_test_invalid', 'message' => t('%name does not accept the value -1.', array('%name' => $instance['label'])), ); @@ -407,7 +416,7 @@ function field_test_field_validate($obj_ /** * 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, $langcode, &$items) { foreach ($items as $delta => $item) { $value = check_plain($item['value']); $items[$delta]['safe'] = $value; @@ -474,8 +483,8 @@ function field_test_field_widget_info() * 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 @@ -577,6 +586,48 @@ function field_test_default_value($obj_t } /** + * Generic op to test _field_invoke behavior. + */ +function field_test_field_test_op($obj_type, $object, $field, $instance, $langcode, &$items) { + return array($langcode => md5(serialize(array($obj_type, $object, $field['field_name'], $langcode, $items)))); +} + +/** + * Generic op to test _field_invoke_multiple behavior. + */ +function field_test_field_test_op_multiple($obj_type, $objects, $field, $instances, $langcode, &$items) { + $result = array(); + foreach ($objects as $id => $object) { + $result[$id] = array($langcode => md5(serialize(array($obj_type, $object, $field['field_name'], $langcode, $items[$id])))); + } + return $result; +} + +/** + * Implement hook_field_languages(). + */ +function field_test_field_languages($obj_type, $field, &$languages) { + if ($field['settings']['test_hook_in']) { + // Add an unavailable language. + $languages[] = 'xx'; + // Remove an available language. + unset($languages[0]); + } +} + +/** + * Helper function to enable entity translations. + */ +function field_test_fieldable_info_translatable($obj_type = NULL, $translatable = NULL) { + $stored_value = &drupal_static(__FUNCTION__, array()); + if (isset($obj_type) && isset($translatable)) { + $stored_value[$obj_type] = $translatable; + _field_info_collate_types(TRUE); + } + return $stored_value; +} + +/** * Store and retrieve keyed data for later verification by unit tests. * * This function is a simple in-memory key-value store with the Index: modules/system/system.test =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.test,v retrieving revision 1.65 diff -u -p -r1.65 system.test --- modules/system/system.test 19 Aug 2009 20:19:37 -0000 1.65 +++ modules/system/system.test 21 Aug 2009 13:28:41 -0000 @@ -495,7 +495,7 @@ class AccessDeniedTestCase extends Drupa $edit = array( 'title' => $this->randomName(10), - 'body' => array(array('value' => $this->randomName(100))), + 'body' => array(FIELD_LANGUAGE_NONE => array(array('value' => $this->randomName(100)))), ); $node = $this->drupalCreateNode($edit); @@ -572,7 +572,7 @@ class PageNotFoundTestCase extends Drupa $edit = array( 'title' => $this->randomName(10), - 'body' => array(array('value' => $this->randomName(100))), + 'body' => array(FIELD_LANGUAGE_NONE => array(array('value' => $this->randomName(100)))), ); $node = $this->drupalCreateNode($edit); @@ -696,9 +696,10 @@ class PageTitleFiltering extends DrupalW drupal_set_title($title, PASS_THROUGH); $this->assertTrue(strpos(drupal_get_title(), '') !== FALSE, t('Tags in title are not converted to entities when $output is PASS_THROUGH.')); // Generate node content. + $langcode = FIELD_LANGUAGE_NONE; $edit = array( 'title' => '!SimpleTest! ' . $title . $this->randomName(20), - 'body[0][value]' => '!SimpleTest! test body' . $this->randomName(200), + "body[$langcode][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/taxonomy/taxonomy.module =================================================================== RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.module,v retrieving revision 1.497 diff -u -p -r1.497 taxonomy.module --- modules/taxonomy/taxonomy.module 19 Aug 2009 13:31:13 -0000 1.497 +++ modules/taxonomy/taxonomy.module 21 Aug 2009 13:28:41 -0000 @@ -1930,7 +1930,7 @@ function taxonomy_field_schema($field) { * Possible error codes: * - 'taxonomy_term_illegal_value': The value is not part of the list of allowed values. */ -function taxonomy_field_validate($obj_type, $object, $field, $instance, $items, &$errors) { +function taxonomy_field_validate($obj_type, $object, $field, $instance, $langcode, $items, &$errors) { $allowed_values = taxonomy_allowed_values($field); $widget = field_info_widget_types($instance['widget']['type']); @@ -2032,7 +2032,7 @@ function taxonomy_allowed_values($field) * This preloads all taxonomy terms for multiple loaded objects at once and * unsets values for invalid terms that do not exist. */ -function taxonomy_field_load($obj_type, $objects, $field, $instances, &$items, $age) { +function taxonomy_field_load($obj_type, $objects, $field, $instances, $langcode, &$items, $age) { $tids = array(); // Collect every possible term attached to any of the fieldable entities. Index: modules/taxonomy/taxonomy.test =================================================================== RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.test,v retrieving revision 1.43 diff -u -p -r1.43 taxonomy.test --- modules/taxonomy/taxonomy.test 4 Aug 2009 06:50:07 -0000 1.43 +++ modules/taxonomy/taxonomy.test 21 Aug 2009 13:28:42 -0000 @@ -453,7 +453,8 @@ class TaxonomyTermTestCase extends Taxon // Post an article. $edit = array(); $edit['title'] = $this->randomName(); - $edit['body[0][value]'] = $this->randomName(); + $langcode = FIELD_LANGUAGE_NONE; + $edit["body[$langcode][0][value]"] = $this->randomName(); $edit['taxonomy[' . $this->vocabulary->vid . ']'] = $term1->tid; $this->drupalPost('node/add/article', $edit, t('Save')); @@ -495,7 +496,8 @@ class TaxonomyTermTestCase extends Taxon // 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(); + $langcode = FIELD_LANGUAGE_NONE; + $edit["body[$langcode][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) { @@ -763,20 +765,21 @@ class TaxonomyTermFieldTestCase extends field_create_instance($this->instance); // Test valid and invalid values with field_attach_validate(). + $langcode = FIELD_LANGUAGE_NONE; $entity = field_test_create_stub_entity(0, 0, FIELD_TEST_BUNDLE); $term = $this->createTerm($this->vocabulary); - $entity->{$this->field_name}[0]['value'] = $term->tid; + $entity->{$this->field_name}[$langcode][0]['value'] = $term->tid; field_attach_validate('test_entity', $entity); try { - $this->assertTrue($entity->{$this->field_name}[0]['value'] == $term->tid, t('Correct term does not cause validation error')); + $this->assertTrue($entity->{$this->field_name}[$langcode][0]['value'] == $term->tid, t('Correct term does not cause validation error')); } catch (FieldValidationException $e) { - $this->assertTrue($entity->{$this->field_name}[0]['value'] != $term->tid, t('Term from wrong vocabulary does not cause validation error')); + $this->assertTrue($entity->{$this->field_name}[$langcode][0]['value'] != $term->tid, t('Term from wrong vocabulary does not cause validation error')); } $entity = field_test_create_stub_entity(0, 0, FIELD_TEST_BUNDLE); $bad_term = $this->createTerm($this->createVocabulary()); - $entity->{$this->field_name}[0]['value'] = $bad_term->tid; + $entity->{$this->field_name}[$langcode][0]['value'] = $bad_term->tid; try { field_attach_validate('test_entity', $entity); } @@ -819,12 +822,13 @@ class TaxonomyTermFieldTestCase extends $term = $this->createTerm($this->vocabulary); // Display creation form. + $langcode = FIELD_LANGUAGE_NONE; $this->drupalGet('test-entity/add/test-bundle'); - $this->assertFieldByName($this->field_name . '[value]', '', t('Widget is displayed')); + $this->assertFieldByName("{$this->field_name}[$langcode][value]", '', t('Widget is displayed')); // Submit with some value. $edit = array( - $this->field_name . '[value]' => array($term->tid), + "{$this->field_name}[$langcode][value]" => array($term->tid), ); $this->drupalPost(NULL, $edit, t('Save')); preg_match('|test-entity/(\d+)/edit|', $this->url, $match); Index: modules/translation/translation.test =================================================================== RCS file: /cvs/drupal/drupal/modules/translation/translation.test,v retrieving revision 1.15 diff -u -p -r1.15 translation.test --- modules/translation/translation.test 11 Aug 2009 11:52:46 -0000 1.15 +++ modules/translation/translation.test 21 Aug 2009 13:28:42 -0000 @@ -60,14 +60,15 @@ class TranslationTestCase extends Drupal // to return to the page then resubmitting the form without a refresh. $edit = array(); $edit['title'] = $this->randomName(); - $edit['body[0][value]'] = $this->randomName(); + $langcode = FIELD_LANGUAGE_NONE; + $edit["body[$langcode][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[$langcode][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 +79,7 @@ class TranslationTestCase extends Drupal // Update translation and mark as updated. $edit = array(); - $edit['body[0][value]'] = $this->randomName(); + $edit["body[$langcode][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 +129,8 @@ class TranslationTestCase extends Drupal function createPage($title, $body, $language) { $edit = array(); $edit['title'] = $title; - $edit['body[0][value]'] = $body; + $langcode = FIELD_LANGUAGE_NONE; + $edit["body[$langcode][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 +155,8 @@ class TranslationTestCase extends Drupal $edit = array(); $edit['title'] = $title; - $edit['body[0][value]'] = $body; + $langcode = FIELD_LANGUAGE_NONE; + $edit["body[$langcode][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/trigger/trigger.test =================================================================== RCS file: /cvs/drupal/drupal/modules/trigger/trigger.test,v retrieving revision 1.15 diff -u -p -r1.15 trigger.test --- modules/trigger/trigger.test 28 Jul 2009 19:18:08 -0000 1.15 +++ modules/trigger/trigger.test 21 Aug 2009 13:28:42 -0000 @@ -38,7 +38,8 @@ class TriggerContentTestCase extends Dru $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); + $langcode = FIELD_LANGUAGE_NONE; + $edit["body[$langcode][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/user/user-profile.tpl.php =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user-profile.tpl.php,v retrieving revision 1.10 diff -u -p -r1.10 user-profile.tpl.php --- modules/user/user-profile.tpl.php 13 Jul 2009 21:09:54 -0000 1.10 +++ modules/user/user-profile.tpl.php 21 Aug 2009 13:28:42 -0000 @@ -16,6 +16,13 @@ * included by modules. $user_profile['user_picture'] is available * for showing the account picture. * + * Field variables: for each field instance attached to the user a corresponding + * variable is defined, e.g. $user->field_example becomes $field_example. When + * needing to access a field's raw values, developers/themers are strongly + * encouraged to use these variables. Otherwise they will have to explicitly + * specify the desired field language, e.g. $user->field_example['en'], thus + * overriding any language negotiation rule that was previously applied. + * * @see user-profile-category.tpl.php * Where the html is handled for the group. * @see user-profile-field.tpl.php Index: modules/field/field.multilingual.inc =================================================================== RCS file: modules/field/field.multilingual.inc diff -N modules/field/field.multilingual.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/field/field.multilingual.inc 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,123 @@ + NULL)); +} + + +/** + * Check if a module is registered as a translation handler for a given entity. + * + * @param $obj_type + * The type of the entity whose fields are to be translated. + * @param $handler + * The name of the handler to be checked. + * @return + * TRUE, if the handler is allowed to manage field translations. + */ +function field_multilingual_check_translation_handler($obj_type, $handler) { + $obj_info = field_info_fieldable_types($obj_type); + return isset($obj_info['translation_handlers'][$handler]); +} + +/** + * Helper function to ensure that a given language code is valid. + * + * Checks whether the given language is one of the enabled languages. Otherwise, + * it returns the current, global language; or the site's default language, if + * the additional parameter $default is TRUE. + * + * @param $langcode + * The language code to validate. + * @param $default + * Whether to return the default language code or the current language code in + * case $langcode is invalid. + * @return + * A valid language code. + */ +function field_multilingual_valid_language($langcode, $default = TRUE) { + $enabled_languages = field_multilingual_content_languages(); + if (in_array($langcode, $enabled_languages)) { + return $langcode; + } + // @todo Currently, node language neutral code is an empty string. Node passes + // $node->language as language parameter to field_attach_form(). We might + // want to unify the two "language neutral" language codes. + if ($langcode === '') { + return FIELD_LANGUAGE_NONE; + } + global $language; + $langcode = $default ? language_default('language') : $language->language; + if (in_array($langcode, $enabled_languages)) { + return $langcode; + } + // @todo Throw a more specific exception. + throw new FieldException('No valid content language could be found.'); +}