Index: modules/field/field.api.php =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.api.php,v retrieving revision 1.27 diff -u -p -r1.27 field.api.php --- modules/field/field.api.php 19 Aug 2009 13:31:12 -0000 1.27 +++ modules/field/field.api.php 19 Aug 2009 20:01:59 -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, FALSE) : ''; + $items[$id][$delta]['safe'] = isset($item['value']) ? check_markup($item['value'], $format, $langcode, FALSE, FALSE) : ''; if ($field['type'] == 'text_with_summary') { - $items[$id][$delta]['safe_summary'] = isset($item['summary']) ? check_markup($item['summary'], $format, $lang, FALSE, FALSE) : ''; + $items[$id][$delta]['safe_summary'] = isset($item['summary']) ? check_markup($item['summary'], $format, $langcode, FALSE, 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, FALSE) : ''; + $items[$delta]['safe'] = isset($item['value']) ? check_markup($item['value'], $format, $langcode, FALSE) : ''; if ($field['type'] == 'text_with_summary') { - $items[$delta]['safe_summary'] = isset($item['summary']) ? check_markup($item['summary'], $format, $lang, FALSE) : ''; + $items[$delta]['safe_summary'] = isset($item['summary']) ? check_markup($item['summary'], $format, $langcode, FALSE) : ''; } } 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 19 Aug 2009 20:04:01 -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,37 @@ 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 19 Aug 2009 20:01:24 -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 19 Aug 2009 20:04:47 -0000 @@ -1,5 +1,5 @@ {$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 19 Aug 2009 20:01:25 -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,21 @@ 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. + // This allow us to match the field data structure: + // $object->{$field_name}[$langcode][$delta][$column]. + // The '#language' key can be used to access the field form element + // when $langcode is unknown. + // The #weight property is inherited from the form field 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 +118,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 +211,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 +292,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 +306,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 +336,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]); } } } @@ -353,7 +370,7 @@ function field_add_more_js($bundle_name, $instance = $form['#fields'][$field_name]['instance']; $form_path = $form['#fields'][$field_name]['form_path']; if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED) { - // Ivnalid + // Invalid $invalid = TRUE; } @@ -383,19 +400,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 +435,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 19 Aug 2009 20:01:25 -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 19 Aug 2009 20:01:25 -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 19 Aug 2009 20:01:25 -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.23 diff -u -p -r1.23 field.module --- modules/field/field.module 19 Aug 2009 13:31:12 -0000 1.23 +++ modules/field/field.module 19 Aug 2009 20:06:14 -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'); /** @@ -68,6 +69,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); @@ -276,13 +284,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'])) { @@ -701,6 +711,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.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 19 Aug 2009 21:07:01 -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.'); +} 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 19 Aug 2009 20:30:07 -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,232 @@ 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()); + } + } + + /** + * Check that that only the correct values are returned by field_multilingual_available_languages. + */ + function testFieldAvailableLanguages() { + // Test translatable fieldable info. + $field = $this->field; + $field['field_name'] .= '_untranslatable'; + $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 if the parameters passed to _field_invoke were correctly forwarded to the callback function. + $this->assertEqual($hash, $result, t('The result for %language is correctly stored.', array('%language' => $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 if _field_invoke correctly uses + // the result of field_multilingual_available_languages. + $extra_languages = mt_rand(1, 4); + for ($i = 0; $i < $extra_languages; ++$i) { + $languages[] = $this->randomString(2); + } + + // For each given language provide some random values. + foreach ($languages as $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 if the parameters passed to _field_invoke were correctly forwarded to the callback function. + $this->assertEqual($hash, $result, t('The result for object %id/%language is correctly stored.', array('%id' => $id, '%language' => $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 +2155,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 19 Aug 2009 20:37: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 to store 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 19 Aug 2009 20:44:04 -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/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 19 Aug 2009 20:01:25 -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/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 19 Aug 2009 20:49:29 -0000 @@ -58,6 +58,13 @@ * - $is_front: Flags true when presented in the front page. * - $logged_in: Flags true when the current user is a logged-in member. * - $is_admin: Flags true when the current user is an administrator. + * + * Field variables: for each field instance attached to the node a corresponding + * variable is defined, e.g. $node->body becomes $body. When needing to access + * 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() Index: modules/simpletest/tests/field_test.module =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/field_test.module,v retrieving revision 1.14 diff -u -p -r1.14 field_test.module --- modules/simpletest/tests/field_test.module 11 Aug 2009 14:59:40 -0000 1.14 +++ modules/simpletest/tests/field_test.module 19 Aug 2009 20:01:25 -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; @@ -478,8 +487,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 @@ -581,6 +590,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/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 19 Aug 2009 20:54:09 -0000 @@ -15,6 +15,13 @@ * is provided which contains data on the user's history. Other data can be * included by modules. $user_profile['user_picture'] is available * for showing the account picture. + * + * Field variables: for each field instance attached to the user a corresponding + * variable is defined, e.g. $user->field_example becomes $field_example. When + * needing to access 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.