Index: modules/field/field.crud.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.crud.inc,v retrieving revision 1.22 diff -u -r1.22 field.crud.inc --- modules/field/field.crud.inc 2 Aug 2009 11:24:21 -0000 1.22 +++ modules/field/field.crud.inc 8 Aug 2009 18:32:34 -0000 @@ -46,6 +46,8 @@ * - cardinality (integer) * The number of values the field can hold. Legal values are any * positive integer or FIELD_CARDINALITY_UNLIMITED. + * - translatable (integer) + * Whether the field is translatable * - locked (integer) * TODO: undefined. * - module (string, read-only) @@ -230,6 +232,7 @@ $field += array( 'cardinality' => 1, + 'translatable' => FALSE, 'locked' => FALSE, 'settings' => array(), ); Index: modules/field/field.default.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.default.inc,v retrieving revision 1.12 diff -u -r1.12 field.default.inc --- modules/field/field.default.inc 2 Aug 2009 11:24:21 -0000 1.12 +++ modules/field/field.default.inc 8 Aug 2009 18:32:34 -0000 @@ -11,17 +11,17 @@ * the corresponding field_attach_[operation]() function. */ -function field_default_extract_form_values($obj_type, $object, $field, $instance, &$items, $form, &$form_state) { +function field_default_extract_form_values($obj_type, $object, $field, $instance, $language, &$items, $form, &$form_state) { $field_name = $field['field_name']; - if (isset($form_state['values'][$field_name])) { - $items = $form_state['values'][$field_name]; + if (isset($form_state['values'][$field_name][$language])) { + $items = $form_state['values'][$field_name][$language]; // Remove the 'value' of the 'add more' button. unset($items[$field_name . '_add_more']); } } -function field_default_submit($obj_type, $object, $field, $instance, &$items, $form, &$form_state) { +function field_default_submit($obj_type, $object, $field, $instance, $language, &$items, $form, &$form_state) { $field_name = $field['field_name']; // Reorder items to account for drag-n-drop reordering. @@ -40,13 +40,18 @@ * This can happen with programmatic saves, or on form-based creation where * the current user doesn't have 'edit' permission for the field. */ -function field_default_insert($obj_type, $object, $field, $instance, &$items) { +function field_default_insert($obj_type, $object, $field, $instance, $language, &$items) { // _field_invoke() populates $items with an empty array if the $object has no // entry for the field, so we check on the $object itself. - if (!property_exists($object, $field['field_name']) && !empty($instance['default_value_function'])) { + // We also check that the current field translation is actually defined before + // assigning it a default value, this way we ensure that only the intended languages + // get a default value, otherwise we could have default values for not yet open + // languages. + if (!empty($instance['default_value_function']) && (!property_exists($object, $field['field_name']) || + (isset($object->{$field['field_name']}[$language]) && count($object->{$field['field_name']}[$language]) == 0))) { $function = $instance['default_value_function']; if (drupal_function_exists($function)) { - $items = $function($obj_type, $object, $field, $instance); + $items = $function($obj_type, $object, $field, $instance, $language); } } } @@ -108,7 +113,7 @@ * ), * ); */ -function field_default_view($obj_type, $object, $field, $instance, $items, $build_mode) { +function field_default_view($obj_type, $object, $field, $instance, $language, $items, $build_mode) { list($id, $vid, $bundle) = field_attach_extract_ids($obj_type, $object); $addition = array(); @@ -138,6 +143,7 @@ '#label_display' => $label_display, '#build_mode' => $build_mode, '#single' => $single, + '#language' => $language, 'items' => array(), ); @@ -173,7 +179,7 @@ return $addition; } -function field_default_prepare_translation($obj_type, $object, $field, $instance, &$items) { +function field_default_prepare_translation($obj_type, $object, $field, $instance, $language, &$items) { $addition = array(); if (isset($object->translation_source->$field['field_name'])) { $addition[$field['field_name']] = $object->translation_source->$field['field_name']; Index: modules/field/field.info.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.info.inc,v retrieving revision 1.11 diff -u -r1.11 field.info.inc --- modules/field/field.info.inc 2 Aug 2009 11:24:21 -0000 1.11 +++ modules/field/field.info.inc 8 Aug 2009 18:32:34 -0000 @@ -117,6 +117,7 @@ // Provide defaults. $fieldable_info += array( 'cacheable' => TRUE, + 'translation_handlers' => array(), 'bundles' => array(), ); $fieldable_info['object keys'] += array( Index: modules/field/field.attach.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.attach.inc,v retrieving revision 1.33 diff -u -r1.33 field.attach.inc --- modules/field/field.attach.inc 16 Jul 2009 10:30:12 -0000 1.33 +++ modules/field/field.attach.inc 8 Aug 2009 18:32:33 -0000 @@ -166,6 +166,7 @@ // Merge default options. $default_options = array( 'default' => FALSE, + 'language' => NULL ); $options += $default_options; @@ -178,32 +179,38 @@ // When in 'single field' mode, only act on the specified field. if (empty($options['field_name']) || $options['field_name'] == $field_name) { $field = field_info_field($field_name); + $field_translations = array(); + $suggested_languages = empty($options['language']) ? NULL : array($options['language']); - // Extract the field values into a separate variable, easily accessed by - // hook implementations. - $items = isset($object->$field_name) ? $object->$field_name : array(); + // Initialize field translations according to the available languages. + foreach (field_multilingual_available_languages($obj_type, $field, $suggested_languages) as $language) { + $field_translations[$language] = isset($object->{$field_name}[$language]) ? $object->{$field_name}[$language] : array(); + } // Invoke the field hook and collect results. $function = $options['default'] ? 'field_default_' . $op : $field['module'] . '_field_' . $op; if (drupal_function_exists($function)) { - $result = $function($obj_type, $object, $field, $instance, $items, $a, $b); - if (isset($result)) { - // For hooks with array results, we merge results together. - // For hooks with scalar results, we collect results in an array. - if (is_array($result)) { - $return = array_merge($return, $result); + // Iterate over all the field translations. + foreach ($field_translations as $language => $items) { + $result = $function($obj_type, $object, $field, $instance, $language, $items, $a, $b); + if (isset($result)) { + // For hooks with array results, we merge results together. + // For hooks with scalar results, we collect results in an array. + if (is_array($result)) { + $return = array_merge($return, $result); + } + else { + $return[] = $result; + } } - else { - $return[] = $result; + + // Populate $items back in the field values, but avoid replacing missing + // fields with an empty array (those are not equivalent on update). + if ($items !== array() || isset($object->{$field_name}[$language])) { + $object->{$field_name}[$language] = $items; } } } - - // Populate field values back in the object, but avoid replacing missing - // fields with an empty array (those are not equivalent on update). - if ($items !== array() || property_exists($object, $field_name)) { - $object->$field_name = $items; - } } } @@ -248,6 +255,7 @@ // Merge default options. $default_options = array( 'default' => FALSE, + 'language' => NULL ); $options += $default_options; @@ -271,10 +279,11 @@ } // Group the corresponding instances and objects. $grouped_instances[$field_name][$id] = $instance; - $grouped_objects[$field_name][$id] = $objects[$id]; - // Extract the field values into a separate variable, easily accessed - // by hook implementations. - $grouped_items[$field_name][$id] = isset($object->$field_name) ? $object->$field_name : array(); + $grouped_objects[$field_name][$id] = &$objects[$id]; + $suggested_languages = empty($options['language']) ? NULL : array($options['language']); + foreach (field_multilingual_available_languages($obj_type, $fields[$field_name], $suggested_languages) as $language) { + $grouped_items[$field_name][$language][$id] = isset($object->{$field_name}[$language]) ? $object->{$field_name}[$language] : array(); + } } } // Initialize the return value for each object. @@ -285,17 +294,20 @@ foreach ($fields as $field_name => $field) { $function = $options['default'] ? 'field_default_' . $op : $field['module'] . '_field_' . $op; if (drupal_function_exists($function)) { - $results = $function($obj_type, $grouped_objects[$field_name], $field, $grouped_instances[$field_name], $grouped_items[$field_name], $a, $b); - if (isset($results)) { - // Collect results by object. - // For hooks with array results, we merge results together. - // For hooks with scalar results, we collect results in an array. - foreach ($results as $id => $result) { - if (is_array($result)) { - $return[$id] = array_merge($return[$id], $result); - } - else { - $return[$id][] = $result; + // Iterate over all the field translations. + foreach ($grouped_items[$field_name] as $language => $items) { + $results = $function($obj_type, $grouped_objects[$field_name], $field, $grouped_instances[$field_name], $language, $grouped_items[$field_name][$language], $a, $b); + if (isset($results)) { + // Collect results by object. + foreach ($results as $id => $result) { + // For hooks with array results, we merge results together. + // For hooks with scalar results, we collect results in an array. + if (is_array($result)) { + $return[$id] = array_merge($return[$id], $result); + } + else { + $return[$id][] = $result; + } } } } @@ -304,8 +316,10 @@ // Populate field values back in the objects, but avoid replacing missing // fields with an empty array (those are not equivalent on update). foreach ($grouped_objects[$field_name] as $id => $object) { - if ($grouped_items[$field_name][$id] !== array() || property_exists($object, $field_name)) { - $object->$field_name = $grouped_items[$field_name][$id]; + foreach ($grouped_items[$field_name] as $language => $items) { + if ($grouped_items[$field_name][$language][$id] !== array() || isset($object->{$field_name}[$language])) { + $object->{$field_name}[$language] = $grouped_items[$field_name][$language][$id]; + } } } } @@ -353,12 +367,17 @@ * The form structure to fill in. * @param $form_state * An associative array containing the current state of the form. + * @param $language + * TODO: The are two ways to enter field values: predefined language and + * user provided language [...] * * TODO : document the resulting $form structure, like we do for * field_attach_view(). */ -function field_attach_form($obj_type, $object, &$form, &$form_state) { - $form += (array) _field_invoke_default('form', $obj_type, $object, $form, $form_state); +function field_attach_form($obj_type, $object, &$form, &$form_state, $language = NULL) { + // If no language is provided use the default site language. + $options = array('language' => field_multilingual_valid_language($language)); + $form += (array) _field_invoke_default('form', $obj_type, $object, $form, $form_state, $options); // Let other modules make changes to the form. foreach (module_implements('field_attach_form') as $module) { @@ -880,17 +899,53 @@ * @return * A structured content array tree for drupal_render(). */ -function field_attach_view($obj_type, $object, $build_mode = 'full') { +function field_attach_view($obj_type, $object, $build_mode = 'full', $language = NULL) { + // If no language is provided use the current UI language. + $options = array('language' => field_multilingual_valid_language($language, FALSE)); + // Let field modules sanitize their data for output. - _field_invoke('sanitize', $obj_type, $object); + _field_invoke('sanitize', $obj_type, $object, $null, $null, $options); - $output = _field_invoke_default('view', $obj_type, $object, $build_mode); + $output = _field_invoke_default('view', $obj_type, $object, $build_mode, $null, $options); // Let other modules make changes after rendering the view. drupal_alter('field_attach_view', $output, $obj_type, $object, $build_mode); return $output; +} + +/** + * Populate the template variables with the field values available for rendering. + * + * The $variables array will be populated with all the field instance values associated + * with the given entity type, keyed by field name; in case of translatable fields the + * language currently chosen for display will be selected. + * + * @param $obj_type + * The type of $object; e.g. 'node' or 'user'. + * @param $object + * The object with fields to render. + * @param $element + * The structured array containing the values ready for rendering. + * @param $variables + * The variables array is passed by reference and will be populated with field values. + */ +function field_attach_preprocess($obj_type, $object, $element, &$variables) { + list(, , $bundle) = field_attach_extract_ids($obj_type, $object); + foreach (field_info_instances($bundle) as $instance) { + $field_name = $instance['field_name']; + if (isset($element[$field_name]['#language'])) { + $language = $element[$field_name]['#language']; + $variables[$field_name] = isset($object->{$field_name}[$language]) ? $object->{$field_name}[$language] : NULL; + } + } + + // Let other modules make changes to the form. + foreach (module_implements('field_attach_preprocess') as $module) { + $function = $module . '_field_attach_preprocess'; + $function($obj_type, $object, $element, $variables); + } } /** Index: modules/field/field.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.module,v retrieving revision 1.21 diff -u -r1.21 field.module --- modules/field/field.module 2 Aug 2009 11:24:21 -0000 1.21 +++ modules/field/field.module 8 Aug 2009 18:32:35 -0000 @@ -12,6 +12,7 @@ */ require(DRUPAL_ROOT . '/modules/field/field.crud.inc'); require(DRUPAL_ROOT . '/modules/field/field.info.inc'); +require(DRUPAL_ROOT . '/modules/field/field.multilingual.inc'); require(DRUPAL_ROOT . '/modules/field/field.attach.inc'); /** @@ -64,6 +65,12 @@ define('FIELD_CARDINALITY_UNLIMITED', -1); /** + * The language code assigned to untranslatable fields. + * (ISO639-2 No linguistic content / Not applicable) + */ +define('FIELD_LANGUAGE_NONE', 'zxx'); + +/** * TODO */ define('FIELD_BEHAVIOR_NONE', 0x0001); @@ -605,6 +612,8 @@ 'label' => check_plain(t($instance['label'])), 'label_display' => $element['#label_display'], 'field_empty' => $field_empty, + 'field_language' => $element['#language'], + 'field_translatable' => $field['translatable'], 'template_files' => array( 'field', 'field-' . $element['#field_name'], Index: modules/field/field.form.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.form.inc,v retrieving revision 1.12 diff -u -r1.12 field.form.inc --- modules/field/field.form.inc 27 Jul 2009 20:19:20 -0000 1.12 +++ modules/field/field.form.inc 8 Aug 2009 18:32:34 -0000 @@ -9,7 +9,7 @@ /** * Create a separate form element for each field. */ -function field_default_form($obj_type, $object, $field, $instance, $items, &$form, &$form_state, $get_delta = NULL) { +function field_default_form($obj_type, $object, $field, $instance, $language, $items, &$form, &$form_state, $get_delta = NULL) { // This could be called with no object, as when a UI module creates a // dummy form to set default values. if ($object) { @@ -52,7 +52,7 @@ // and we are displaying an individual element, process the multiple value // form. if (!isset($get_delta) && field_behaviors_widget('multiple values', $instance) == FIELD_BEHAVIOR_DEFAULT) { - $form_element = field_multiple_value_form($field, $instance, $items, $form, $form_state); + $form_element = field_multiple_value_form($field, $instance, $language, $items, $form, $form_state); } // If the widget is handling multiple values (e.g Options), // or if we are displaying an individual element, just get a single form @@ -93,7 +93,15 @@ '#weight' => $instance['weight'], ); - $addition[$field['field_name']] = array_merge($form_element, $defaults); + $form_element = array_merge($form_element, $defaults); + + $addition[$field['field_name']] = array( + '#tree' => TRUE, + '#weight' => $form_element['#weight'], + '#language' => $language, + $language => $form_element, + ); + $form['#fields'][$field['field_name']]['form_path'] = array($field['field_name']); } @@ -108,7 +116,7 @@ * - AHAH-'add more' button * - drag-n-drop value reordering */ -function field_multiple_value_form($field, $instance, $items, &$form, &$form_state) { +function field_multiple_value_form($field, $instance, $language, $items, &$form, &$form_state) { $field = field_info_field($instance['field_name']); $field_name = $field['field_name']; @@ -202,9 +210,11 @@ '#field_name' => $field_name, '#bundle' => $instance['bundle'], '#attributes' => array('class' => 'field-add-more-submit'), + '#language' => $language, ); } } + return $form_element; } @@ -281,9 +291,9 @@ /** * Transfer field-level validation errors to widgets. */ -function field_default_form_errors($obj_type, $object, $field, $instance, $items, $form, $errors) { +function field_default_form_errors($obj_type, $object, $field, $instance, $language, $items, $form, $errors) { $field_name = $field['field_name']; - if (!empty($errors[$field_name])) { + if (!empty($errors[$field_name][$language])) { $function = $instance['widget']['module'] . '_field_widget_error'; $function_exists = drupal_function_exists($function); @@ -295,10 +305,10 @@ } $multiple_widget = field_behaviors_widget('multiple values', $instance) != FIELD_BEHAVIOR_DEFAULT; - foreach ($errors[$field_name] as $delta => $delta_errors) { + foreach ($errors[$field_name][$language] as $delta => $delta_errors) { // For multiple single-value widgets, pass errors by delta. // For a multiple-value widget, all errors are passed to the main widget. - $error_element = $multiple_widget ? $element : $element[$delta]; + $error_element = $multiple_widget ? $element[$language] : $element[$language][$delta]; foreach ($delta_errors as $error) { if ($function_exists) { $function($error_element, $error); @@ -325,8 +335,9 @@ // Make the changes we want to the form state. $field_name = $form_state['clicked_button']['#field_name']; + $language = $form_state['clicked_button']['#language']; if ($form_state['values'][$field_name . '_add_more']) { - $form_state['field_item_count'][$field_name] = count($form_state['values'][$field_name]); + $form_state['field_item_count'][$field_name] = count($form_state['values'][$field_name][$language]); } } } @@ -358,7 +369,7 @@ $instance = $form['#fields'][$field_name]['instance']; $form_path = $form['#fields'][$field_name]['form_path']; if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED) { - // Ivnalid + // Invalid $invalid = TRUE; } @@ -388,19 +399,25 @@ // Reset cached ids, so that they don't affect the actual form we output. drupal_static_reset('form_clean_id'); + // Ensure that a valid language is provided. + $language = key($_POST[$field_name]); + if ($language != FIELD_LANGUAGE_NONE) { + $language = field_multilingual_valid_language($language); + } + // Sort the $form_state['values'] we just built *and* the incoming $_POST data // according to d-n-d reordering. - unset($form_state['values'][$field_name][$field['field_name'] . '_add_more']); - foreach ($_POST[$field_name] as $delta => $item) { - $form_state['values'][$field_name][$delta]['_weight'] = $item['_weight']; + unset($form_state['values'][$field_name][$language][$field['field_name'] . '_add_more']); + foreach ($_POST[$field_name][$language] as $delta => $item) { + $form_state['values'][$field_name][$language][$delta]['_weight'] = $item['_weight']; } - $form_state['values'][$field_name] = _field_sort_items($field, $form_state['values'][$field_name]); - $_POST[$field_name] = _field_sort_items($field, $_POST[$field_name]); + $form_state['values'][$field_name][$language] = _field_sort_items($field, $form_state['values'][$field_name][$language]); + $_POST[$field_name][$language] = _field_sort_items($field, $_POST[$field_name][$language]); // Build our new form element for the whole field, asking for one more element. - $form_state['field_item_count'] = array($field_name => count($_POST[$field_name]) + 1); - $items = $form_state['values'][$field_name]; - $form_element = field_default_form(NULL, NULL, $field, $instance, $items, $form, $form_state); + $form_state['field_item_count'] = array($field_name => count($_POST[$field_name][$language]) + 1); + $items = $form_state['values'][$field_name][$language]; + $form_element = field_default_form(NULL, NULL, $field, $instance, $language, $items, $form, $form_state); // Let other modules alter it. drupal_alter('form', $form_element, array(), 'field_add_more_js'); @@ -417,8 +434,8 @@ // Build the new form against the incoming $_POST values so that we can // render the new element. - $delta = max(array_keys($_POST[$field_name])) + 1; - $_POST[$field_name][$delta]['_weight'] = $delta; + $delta = max(array_keys($_POST[$field_name][$language])) + 1; + $_POST[$field_name][$language][$delta]['_weight'] = $delta; $form_state = form_state_defaults(); $form_state['input'] = $_POST; $form = form_builder($_POST['form_id'], $form, $form_state); Index: modules/field/field.info =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.info,v retrieving revision 1.4 diff -u -r1.4 field.info --- modules/field/field.info 8 Jun 2009 09:23:51 -0000 1.4 +++ modules/field/field.info 8 Aug 2009 18:32:34 -0000 @@ -9,6 +9,7 @@ files[] = field.crud.inc files[] = field.info.inc files[] = field.default.inc +files[] = field.multilingual.inc files[] = field.attach.inc files[] = field.form.inc files[] = field.test Index: modules/field/field.install =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.install,v retrieving revision 1.9 diff -u -r1.9 field.install --- modules/field/field.install 28 May 2009 10:05:32 -0000 1.9 +++ modules/field/field.install 8 Aug 2009 18:32:34 -0000 @@ -63,6 +63,12 @@ 'not null' => TRUE, 'default' => 0, ), + 'translatable' => array( + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), 'active' => array( 'type' => 'int', 'size' => 'tiny', Index: modules/field/theme/field.tpl.php =================================================================== RCS file: /cvs/drupal/drupal/modules/field/theme/field.tpl.php,v retrieving revision 1.3 diff -u -r1.3 field.tpl.php --- modules/field/theme/field.tpl.php 22 Jun 2009 09:10:04 -0000 1.3 +++ modules/field/theme/field.tpl.php 8 Aug 2009 18:32:35 -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 -r1.19 node.tpl.php --- modules/node/node.tpl.php 6 Aug 2009 05:05:59 -0000 1.19 +++ modules/node/node.tpl.php 8 Aug 2009 18:32:36 -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 + * the field raw values developers/themers are strongly encouraged to use these + * variables, otherwise they will have to explicitly specify the desired field + * language, e.g. $node->body['en'], thus overriding any language negotiation + * rule previously applied. * * @see template_preprocess() * @see template_preprocess_node() Index: modules/node/node.pages.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.pages.inc,v retrieving revision 1.73 diff -u -r1.73 node.pages.inc --- modules/node/node.pages.inc 4 Aug 2009 06:44:48 -0000 1.73 +++ modules/node/node.pages.inc 8 Aug 2009 18:32:36 -0000 @@ -284,7 +284,7 @@ $form['#theme'] = array($node->type . '_node_form', 'node_form'); $form['#builder_function'] = 'node_form_submit_build_node'; - field_attach_form('node', $node, $form, $form_state); + field_attach_form('node', $node, $form, $form_state, $node->language); return $form; } Index: modules/field/modules/field_sql_storage/field_sql_storage.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/field_sql_storage/field_sql_storage.module,v retrieving revision 1.17 diff -u -r1.17 field_sql_storage.module --- modules/field/modules/field_sql_storage/field_sql_storage.module 15 Jul 2009 17:55:18 -0000 1.17 +++ modules/field/modules/field_sql_storage/field_sql_storage.module 8 Aug 2009 18:32:35 -0000 @@ -144,8 +144,16 @@ 'not null' => TRUE, 'description' => 'The sequence number for this data item, used for multi-value fields', ), + // TODO: Consider an integer field for 'language'. + 'language' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The language for this data item.', + ), ), - 'primary key' => array('etid', 'entity_id', 'deleted', 'delta'), + 'primary key' => array('etid', 'entity_id', 'deleted', 'delta', 'language'), // TODO : index on 'bundle' ); @@ -169,7 +177,7 @@ $revision = $current; $revision['description'] = "Revision archive storage for {$deleted}field {$field['id']} ({$field['field_name']})"; $revision['revision_id']['description'] = 'The entity revision id this data is attached to'; - $revision['primary key'] = array('etid', 'revision_id', 'deleted', 'delta'); + $revision['primary key'] = array('etid', 'revision_id', 'deleted', 'delta', 'language'); return array( _field_sql_storage_tablename($field) => $current, @@ -215,7 +223,7 @@ if (!isset($skip_fields[$field_name]) && (!isset($options['field_name']) || $options['field_name'] == $instance['field_name'])) { $objects[$id]->{$field_name} = array(); $field_ids[$field_name][] = $load_current ? $id : $vid; - $delta_count[$id][$field_name] = 0; + $delta_count[$id][$field_name] = array(); } } } @@ -229,12 +237,17 @@ ->condition('etid', $etid) ->condition($load_current ? 'entity_id' : 'revision_id', $ids, 'IN') ->condition('deleted', 0) + ->condition('language', field_multilingual_available_languages($obj_type, $field), 'IN') ->orderBy('delta'); $results = $query->execute(); foreach ($results as $row) { - if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta_count[$row->entity_id][$field_name] < $field['cardinality']) { + if (!isset($delta_count[$row->entity_id][$field_name][$row->language])) { + $delta_count[$row->entity_id][$field_name][$row->language] = 0; + } + + if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta_count[$row->entity_id][$field_name][$row->language] < $field['cardinality']) { $item = array(); // For each column declared by the field, populate the item // from the prefixed database column. @@ -244,8 +257,8 @@ } // Add the item to the field values for the entity. - $objects[$row->entity_id]->{$field_name}[] = $item; - $delta_count[$row->entity_id][$field_name]++; + $objects[$row->entity_id]->{$field_name}[$row->language][] = $item; + $delta_count[$row->entity_id][$field_name][$row->language]++; } } } @@ -275,17 +288,33 @@ // Function property_exists() is slower, so we catch the more frequent cases // where it's an empty array with the faster isset(). if (isset($object->$field_name) || property_exists($object, $field_name)) { + $available_languages = field_multilingual_available_languages($obj_type, $field); + $available_translations = is_array($object->$field_name) ? array_intersect($available_languages, array_keys($object->$field_name)) : FALSE; + // Delete and insert, rather than update, in case a value was added. - if ($op == FIELD_STORAGE_UPDATE) { - db_delete($table_name)->condition('etid', $etid)->condition('entity_id', $id)->execute(); + // If no translation is available, empty the field for all the available languages. + if ($op == FIELD_STORAGE_UPDATE && count($available_translations)) { + $languages = empty($object->$field_name) ? $available_languages : $available_translations; + + db_delete($table_name) + ->condition('etid', $etid) + ->condition('entity_id', $id) + ->condition('language', $languages, 'IN') + ->execute(); + if (isset($vid)) { - db_delete($revision_name)->condition('etid', $etid)->condition('entity_id', $id)->condition('revision_id', $vid)->execute(); + db_delete($revision_name) + ->condition('etid', $etid) + ->condition('entity_id', $id) + ->condition('revision_id', $vid) + ->condition('language', $languages, 'IN') + ->execute(); } } - if ($object->$field_name) { + if (!empty($available_translations)) { // Prepare the multi-insert query. - $columns = array('etid', 'entity_id', 'revision_id', 'bundle', 'delta'); + $columns = array('etid', 'entity_id', 'revision_id', 'bundle', 'delta', 'language'); foreach ($field['columns'] as $column => $attributes) { $columns[] = _field_sql_storage_columnname($field_name, $column); } @@ -294,25 +323,30 @@ $revision_query = db_insert($revision_name)->fields($columns); } - $delta_count = 0; - foreach ($object->$field_name as $delta => $item) { - $record = array( - 'etid' => $etid, - 'entity_id' => $id, - 'revision_id' => $vid, - 'bundle' => $bundle, - 'delta' => $delta, - ); - foreach ($field['columns'] as $column => $attributes) { - $record[_field_sql_storage_columnname($field_name, $column)] = isset($item[$column]) ? $item[$column] : NULL; - } - $query->values($record); - if (isset($vid)) { - $revision_query->values($record); - } - - if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED && ++$delta_count == $field['cardinality']) { - break; + foreach ($available_translations as $language) { + if ($items = $object->{$field_name}[$language]) { + $delta_count = 0; + foreach ($items as $delta => $item) { + $record = array( + 'etid' => $etid, + 'entity_id' => $id, + 'revision_id' => $vid, + 'bundle' => $bundle, + 'delta' => $delta, + 'language' => $language, + ); + foreach ($field['columns'] as $column => $attributes) { + $record[_field_sql_storage_columnname($field_name, $column)] = isset($item[$column]) ? $item[$column] : NULL; + } + $query->values($record); + if (isset($vid)) { + $revision_query->values($record); + } + + if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED && ++$delta_count == $field['cardinality']) { + break; + } + } } } Index: modules/user/user-profile.tpl.php =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user-profile.tpl.php,v retrieving revision 1.10 diff -u -r1.10 user-profile.tpl.php --- modules/user/user-profile.tpl.php 13 Jul 2009 21:09:54 -0000 1.10 +++ modules/user/user-profile.tpl.php 8 Aug 2009 18:32:36 -0000 @@ -15,6 +15,13 @@ * is provided which contains data on the user's history. Other data can be * included by modules. $user_profile['user_picture'] is available * for showing the account picture. + * + * Field variables: for each field instance attached to the user a corresponding + * variable is defined, e.g. $user->field_example becomes $field_example. When + * needing to access the field raw values developers/themers are strongly encouraged + * to use these variables, otherwise they will have to explicitly specify the desired + * field language, e.g. $user->field_example['en'], thus overriding any language + * negotiation rule previously applied. * * @see user-profile-category.tpl.php * Where the html is handled for the group. Index: modules/field/modules/text/text.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/text/text.module,v retrieving revision 1.17 diff -u -r1.17 text.module --- modules/field/modules/text/text.module 4 Aug 2009 06:38:56 -0000 1.17 +++ modules/field/modules/text/text.module 8 Aug 2009 18:32:35 -0000 @@ -135,7 +135,7 @@ * - 'text_value_max_length': The value exceeds the maximum length. * - 'text_summary_max_length': The summary exceeds the maximum length. */ -function text_field_validate($obj_type, $object, $field, $instance, $items, &$errors) { +function text_field_validate($obj_type, $object, $field, $instance, $language, $items, &$errors) { foreach ($items as $delta => $item) { foreach (array('value' => t('full text'), 'summary' => t('summary')) as $column => $desc) { if (!empty($item[$column])) { @@ -148,7 +148,7 @@ $message = t('%name: the summary may not be longer than %max characters.', array('%name' => $instance['label'], '%max' => $field['settings']['max_length'])); break; } - $errors[$field['field_name']][$delta][] = array( + $errors[$field['field_name']][$language][$delta][] = array( 'error' => "text_{$column}_length", 'message' => $message, ); @@ -166,9 +166,7 @@ * separately. * @see text_field_sanitize(). */ -function text_field_load($obj_type, $objects, $field, $instances, &$items) { - global $language; - +function text_field_load($obj_type, $objects, $field, $instances, $language, &$items) { foreach ($objects as $id => $object) { foreach ($items[$id] as $delta => $item) { if (!empty($instances[$id]['settings']['text_processing'])) { @@ -176,10 +174,9 @@ // handled by text_field_sanitize(). $format = $item['format']; if (filter_format_allowcache($format)) { - $lang = isset($object->language) ? $object->language : $language->language; - $items[$id][$delta]['safe'] = isset($item['value']) ? check_markup($item['value'], $format, $lang, FALSE, FALSE) : ''; + $items[$id][$delta]['safe'] = isset($item['value']) ? check_markup($item['value'], $format, $language, FALSE, FALSE) : ''; if ($field['type'] == 'text_with_summary') { - $items[$id][$delta]['safe_summary'] = isset($item['summary']) ? check_markup($item['summary'], $format, $lang, FALSE, FALSE) : ''; + $items[$id][$delta]['safe_summary'] = isset($item['summary']) ? check_markup($item['summary'], $format, $language, FALSE, FALSE) : ''; } } } @@ -198,7 +195,7 @@ * * @see text_field_load() */ -function text_field_sanitize($obj_type, $object, $field, $instance, &$items) { +function text_field_sanitize($obj_type, $object, $field, $instance, $language, &$items) { global $language; foreach ($items as $delta => $item) { // Only sanitize items which were not already processed inside Index: modules/field/field.multilingual.inc =================================================================== RCS file: modules/field/field.multilingual.inc diff -N modules/field/field.multilingual.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/field/field.multilingual.inc 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,117 @@ + NULL)); +} + + +/** + * Check if a module is registered a s 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 which has to be checked. + * @return + * A boolean indicating 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 which ensures the the given language code is valid. + * + * Check if the given language is one of the enabled languages, otherwise + * depnding on the $default variable's value return the default language code + * or the current language one. + * + * @params $language + * The language code to be checked. + * @params $default + * Whether return the default language code or the current one. + * @returns + * A valid language code. + */ +function field_multilingual_valid_language($language, $default = TRUE) { + $enabled_languages = field_multilingual_content_languages(); + if (in_array($language, $enabled_languages)) { + return $language; + } + // Currently node language neutral code is an empty string. + // TODO: we might want to unify the language codes. + if ($language === '') { + return FIELD_LANGUAGE_NONE; + } + global $language; + $valid_language = $default ? language_default('language') : $language->language; + if (in_array($valid_language, $enabled_languages)) { + return $valid_language; + } + // TODO: throw a more specific exception + throw new FieldException('No valid content language could be found'); +}