Index: modules/locale/locale.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/locale/locale.module,v
retrieving revision 1.258
diff -u -r1.258 locale.module
--- modules/locale/locale.module	31 Aug 2009 17:06:09 -0000	1.258
+++ modules/locale/locale.module	14 Sep 2009 13:30:26 -0000
@@ -326,6 +326,17 @@
         '#value' => $default->language
       );
     }
+    $form['#submit'][] = 'locale_field_node_form_submit';
+  }
+}
+
+/**
+ * Node form submit handler.
+ */
+function locale_field_node_form_submit($form, &$form_state) {
+  if (field_multilingual_check_translation_handlers('node', 'locale')) {
+    module_load_include('inc', 'locale', 'locale.field');
+    locale_field_node_form_update_field_language($form, $form_state);
   }
 }
 
@@ -343,6 +354,24 @@
   );
 }
 
+/**
+ * Implement hook_entity_info_alter().
+ */
+function locale_entity_info_alter(&$entity_info) {
+  $entity_info['node']['translation_handlers']['locale'] = TRUE;
+}
+
+/**
+ * Implement hook_field_attach_view_alter().
+ */
+function locale_field_attach_view_alter($output, $obj_type, $object, $build_mode, $langcode) {
+  // @todo: Avoid recursion.
+  if (variable_get('locale_field_fallback_view', TRUE)) {
+    module_load_include('inc', 'locale', 'locale.field');
+    locale_field_fallback_view($output, $obj_type, $object, $build_mode, $langcode);
+  }
+}
+
 // ---------------------------------------------------------------------------------
 // Locale core functionality
 
Index: modules/locale/locale.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/locale/locale.test,v
retrieving revision 1.40
diff -u -r1.40 locale.test
--- modules/locale/locale.test	28 Aug 2009 14:40:12 -0000	1.40
+++ modules/locale/locale.test	14 Sep 2009 13:30:27 -0000
@@ -16,6 +16,7 @@
  *  - a functional test for configuring a different path alias per language;
  *  - a functional test for configuring a different path alias per language;
  *  - a functional test for multilingual support by content type and on nodes.
+ *  - a functional test for multilingual fields.
  */
 
 
@@ -1601,3 +1602,67 @@
     $this->assertText($test['expect'], $test['message']);
   }
 }
+
+class LocaleMultilingualFieldsFunctionalTest extends DrupalWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Multilingual fields',
+      'description' => 'Test multilingual support for fields.',
+      'group' => 'Locale',
+    );
+  }
+  
+  function setUp() {
+    parent::setUp('locale');
+  }
+  
+  function testMultilingualNodeForm() {
+    // Setup users.
+    $admin_user = $this->drupalCreateUser(array('administer languages', 'administer content types', 'access administration pages', 'create page content', 'edit own page content'));
+    $this->drupalLogin($admin_user);
+    
+    // Add a new language.
+    require_once DRUPAL_ROOT . '/includes/locale.inc';
+    locale_add_language('it', 'Italian', 'Italiano', LANGUAGE_LTR, '', '', TRUE, FALSE);
+    
+    // Set page content type to use multilingual support.
+    $this->drupalGet('admin/structure/node-type/page');
+    $this->assertText(t('Multilingual support'), t('Multilingual support fieldset present on content type configuration form.'));
+    $edit = array(
+      'language_content_type' => 1,
+    );
+    $this->drupalPost('admin/structure/node-type/page', $edit, t('Save content type'));
+    $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Page')), t('Page content type has been updated.'));
+
+    // Create page content.
+    $langcode = FIELD_LANGUAGE_NONE;
+    $body_key = "body[$langcode][0][value]";
+    $body_value = $this->randomName(16);
+    // Create node to edit.
+    $edit = array();
+    $edit['title'] = $this->randomName(8);
+    $edit[$body_key] = $body_value;
+    $edit['language'] = 'en';
+    $this->drupalPost('node/add/page', $edit, t('Save'));
+
+    // Check that the node exists in the database.
+    $node = $this->drupalGetNodeByTitle($edit['title']);
+    $this->assertTrue($node, t('Node found in database.'));
+
+    $assert = isset($node->body['en']) && !isset($node->body[FIELD_LANGUAGE_NONE]) && $node->body['en'][0]['value'] == $body_value;
+    $this->assertTrue($assert, t('Field language correctly set.'));
+
+    // Change node language.
+    $this->drupalGet("node/$node->nid/edit");
+    $edit = array(
+      'title' => $this->randomName(8),
+      'language' => 'it'
+    );
+    $this->drupalPost(NULL, $edit, t('Save'));
+    $node = $this->drupalGetNodeByTitle($edit['title']);
+    $this->assertTrue($node, t('Node found in database.'));
+
+    $assert = isset($node->body['it']) && !isset($node->body['en']) && $node->body['it'][0]['value'] == $body_value;
+    $this->assertTrue($assert, t('Field language correctly changed.'));
+  }
+}
Index: modules/locale/locale.info
===================================================================
RCS file: /cvs/drupal/drupal/modules/locale/locale.info,v
retrieving revision 1.11
diff -u -r1.11 locale.info
--- modules/locale/locale.info	8 Jun 2009 09:23:52 -0000	1.11
+++ modules/locale/locale.info	14 Sep 2009 13:30:26 -0000
@@ -6,4 +6,5 @@
 core = 7.x
 files[] = locale.module
 files[] = locale.install
+files[] = locale.field.inc
 files[] = locale.test
Index: includes/bootstrap.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/bootstrap.inc,v
retrieving revision 1.304
diff -u -r1.304 bootstrap.inc
--- includes/bootstrap.inc	14 Sep 2009 07:43:11 -0000	1.304
+++ includes/bootstrap.inc	14 Sep 2009 13:30:26 -0000
@@ -1704,6 +1704,38 @@
 }
 
 /**
+ * Return the possible fallback languages ordered by language negotiation
+ * settings and language weight.
+ * 
+ * @return
+ *   An array of language codes.
+ */
+function language_get_fallback_candidates() {
+  $fallback_candidates = &drupal_static(__FUNCTION__);
+
+  if (!isset($fallback_candidates)) {
+    $fallback_candidates = array();
+    // @todo Exploit content language negotiation rules.
+
+    // Get languages ordered by weight.
+    // Use array keys to avoid duplicated entries.
+    foreach (language_list('weight') as $languages) {
+      foreach ($languages as $language) {
+        $fallback_candidates[$language->language] = NULL;
+      }
+    }
+
+    $fallback_candidates = array_keys($fallback_candidates);
+    $fallback_candidates[] = FIELD_LANGUAGE_NONE;
+
+    // Let other modules hook in and add/change candidates.
+    drupal_alter('language_get_fallback_candidates', $fallback_candidates);
+  }
+
+  return $fallback_candidates;
+}
+
+/**
  * If Drupal is behind a reverse proxy, we use the X-Forwarded-For header
  * instead of $_SERVER['REMOTE_ADDR'], which would be the IP address of
  * the proxy server, and not the client's. If Drupal is run in a cluster
Index: modules/path/path.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/path/path.test,v
retrieving revision 1.20
diff -u -r1.20 path.test
--- modules/path/path.test	28 Aug 2009 14:40:12 -0000	1.20
+++ modules/path/path.test	14 Sep 2009 13:30:29 -0000
@@ -213,8 +213,7 @@
     $this->clickLink(t('add translation'));
     $edit = array();
     $edit['title'] = $this->randomName();
-    $langcode = FIELD_LANGUAGE_NONE;
-    $edit["body[$langcode][0][value]"] = $this->randomName();
+    $edit["body[fr][0][value]"] = $this->randomName();
     $edit['path'] = $this->randomName();
     $this->drupalPost(NULL, $edit, t('Save'));
 
Index: modules/field/field.multilingual.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.multilingual.inc,v
retrieving revision 1.1
diff -u -r1.1 field.multilingual.inc
--- modules/field/field.multilingual.inc	22 Aug 2009 00:58:52 -0000	1.1
+++ modules/field/field.multilingual.inc	14 Sep 2009 13:30:26 -0000
@@ -31,8 +31,8 @@
   $field_name = $field['field_name'];
 
   if (!isset($field_languages[$field_name]) || !empty($suggested_languages)) {
-    $obj_info = field_info_fieldable_types($obj_type);
-    if (!empty($obj_info['translation_handlers']) && $field['translatable']) {
+    $translation_handlers = field_multilingual_check_translation_handlers($obj_type);
+    if ($translation_handlers && $field['translatable']) {
       $available_languages = field_multilingual_content_languages();
       // The returned languages are a subset of the intersection of enabled ones
       // and suggested ones.
@@ -71,10 +71,12 @@
   return array_keys(language_list() + array(FIELD_LANGUAGE_NONE => NULL));
 }
 
-
 /**
  * Check if a module is registered as a translation handler for a given entity.
  *
+ * If no handler is passed, simply check if there is any translation hanlder
+ * defined for the give entity type.
+ *
  * @param $obj_type
  *   The type of the entity whose fields are to be translated.
  * @param $handler
@@ -82,9 +84,9 @@
  * @return
  *   TRUE, if the handler is allowed to manage field translations.
  */
-function field_multilingual_check_translation_handler($obj_type, $handler) {
+function field_multilingual_check_translation_handlers($obj_type, $handler = NULL) {
   $obj_info = field_info_fieldable_types($obj_type);
-  return isset($obj_info['translation_handlers'][$handler]);
+  return isset($handler) ? isset($obj_info['translation_handlers'][$handler]) : !empty($obj_info['translation_handlers']);
 }
 
 /**
Index: modules/node/node.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/node/node.module,v
retrieving revision 1.1122
diff -u -r1.1122 node.module
--- modules/node/node.module	11 Sep 2009 04:06:39 -0000	1.1122
+++ modules/node/node.module	14 Sep 2009 13:30:29 -0000
@@ -542,6 +542,7 @@
       $field = array(
         'field_name' => 'body',
         'type' => 'text_with_summary',
+        'translatable' => TRUE,
       );
       $field = field_create_field($field);
     }
Index: modules/translation/translation.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/translation/translation.test,v
retrieving revision 1.17
diff -u -r1.17 translation.test
--- modules/translation/translation.test	22 Aug 2009 00:58:55 -0000	1.17
+++ modules/translation/translation.test	14 Sep 2009 13:30:30 -0000
@@ -68,7 +68,7 @@
 
     // Update original and mark translation as outdated.
     $edit = array();
-    $edit["body[$langcode][0][value]"] = $this->randomName();
+    $edit["body[$node->language][0][value]"] = $this->randomName();
     $edit['translation[retranslate]'] = TRUE;
     $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));
     $this->assertRaw(t('Page %title has been updated.', array('%title' => $node_title)), t('Original node updated.'));
@@ -79,7 +79,7 @@
 
     // Update translation and mark as updated.
     $edit = array();
-    $edit["body[$langcode][0][value]"] = $this->randomName();
+    $edit["body[$node_translation->language][0][value]"] = $this->randomName();
     $edit['translation[status]'] = FALSE;
     $this->drupalPost('node/' . $node_translation->nid . '/edit', $edit, t('Save'));
     $this->assertRaw(t('Page %title has been updated.', array('%title' => $node_translation_title)), t('Translated node updated.'));
@@ -155,8 +155,7 @@
 
     $edit = array();
     $edit['title'] = $title;
-    $langcode = FIELD_LANGUAGE_NONE;
-    $edit["body[$langcode][0][value]"] = $body;
+    $edit["body[$language][0][value]"] = $body;
     $this->drupalPost(NULL, $edit, t('Save'));
     $this->assertRaw(t('Page %title has been created.', array('%title' => $edit['title'])), t('Translation created.'));
 
Index: modules/locale/locale.field.inc
===================================================================
RCS file: modules/locale/locale.field.inc
diff -N modules/locale/locale.field.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/locale/locale.field.inc	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,68 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Field API multilingual handling.
+ */
+
+/**
+ * Node form submit handler. Update the field language according to the node
+ * language, changing the previous language if necessary.
+ */
+function locale_field_node_form_update_field_language($form, &$form_state, $reset_previous = TRUE) {
+  $node = (object) $form_state['values'];
+  $available_languages = field_multilingual_content_languages();
+  // TODO: unify language neutral language codes
+  $selected_language = empty($node->language) ? FIELD_LANGUAGE_NONE : $node->language;
+  list(, , $bundle) = field_extract_ids('node', $node);
+  foreach (field_info_instances($bundle) as $instance) {
+    $field_name = $instance['field_name'];
+    $field = field_info_field($field_name);
+    $previous_language = $form[$field_name]['#language'];
+    // Handle a possible language change: previous language values are deleted, new ones are inserted.
+    if ($field['translatable'] && $previous_language != $selected_language) {
+      $form_state['values'][$field_name][$selected_language] = $node->{$field_name}[$previous_language];
+      if ($reset_previous) {
+        $form_state['values'][$field_name][$previous_language] = array();
+      }
+    }
+  }
+}
+
+/**
+ * Apply fallback rules to the given object. Parameters are the same of 
+ * hook_field_attach_view().
+ */
+function locale_field_fallback_view(&$output, $obj_type, $object, $build_mode, $langcode) {
+  if (field_multilingual_check_translation_handlers($obj_type, 'locale')) {
+    // Lazy init fallback values and candidates to avoid unnecessary calls.
+    $fallback_values = array();
+    $fallback_candidates = NULL;
+    list(, , $bundle) = field_extract_ids($obj_type, $object);
+    foreach (field_info_instances($bundle) as $instance) {
+      $field_name = $instance['field_name'];
+      $field = field_info_field($field_name);
+      // If the items array is empty then we have a missing field translation.
+      // @todo Verify this assumption.
+      if (empty($output[$field_name]['items'])) {
+        if (!isset($fallback_candidates)) {
+          $fallback_candidates = language_get_fallback_candidates();
+        }
+        foreach ($fallback_candidates as $fallback) {
+          // Again if we have a non-empty array we assume the field translation is valid.
+          if (!empty($object->{$field_name}[$fallback])) {
+            // Cache fallback values per language as fields might have different
+            // fallback values.
+            if (!isset($fallback_values[$fallback])) {
+              $fallback_values[$fallback] = field_attach_view($obj_type, $object, $build_mode, $fallback);
+            }
+            // We are done, skip to the next field.
+            $output[$field_name] = $fallback_values[$fallback][$field_name];
+            break;
+          }
+        }
+      }
+    }
+  }
+}
