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 @@
 <?php
-// $Id: field.default.inc,v 1.15 2009/08/19 13:31:12 webchick Exp $
+// $Id: field.default.inc,v 1.14 2009/08/13 01:50:00 webchick Exp $
 
 /**
  * @file
@@ -11,17 +11,17 @@
  * the corresponding field_attach_[operation]() function.
  */
 
-function field_default_extract_form_values($obj_type, $object, $field, $instance, &$items, $form, &$form_state) {
+function field_default_extract_form_values($obj_type, $object, $field, $instance, $langcode, &$items, $form, &$form_state) {
   $field_name = $field['field_name'];
 
-  if (isset($form_state['values'][$field_name])) {
-    $items = $form_state['values'][$field_name];
+  if (isset($form_state['values'][$field_name][$langcode])) {
+    $items = $form_state['values'][$field_name][$langcode];
     // Remove the 'value' of the 'add more' button.
     unset($items[$field_name . '_add_more']);
   }
 }
 
-function field_default_submit($obj_type, $object, $field, $instance, &$items, $form, &$form_state) {
+function field_default_submit($obj_type, $object, $field, $instance, $langcode, &$items, $form, &$form_state) {
   $field_name = $field['field_name'];
 
   // Reorder items to account for drag-n-drop reordering.
@@ -40,19 +40,25 @@ function field_default_submit($obj_type,
  * This can happen with programmatic saves, or on form-based creation where
  * the current user doesn't have 'edit' permission for the field.
  */
-function field_default_insert($obj_type, $object, $field, $instance, &$items) {
+function field_default_insert($obj_type, $object, $field, $instance, $langcode, &$items) {
   // _field_invoke() populates $items with an empty array if the $object has no
   // entry for the field, so we check on the $object itself.
-  if (empty($object) || !property_exists($object, $field['field_name'])) {
-    $items = field_get_default_value($obj_type, $object, $field, $instance);
+  // We also check that the current field translation is actually defined before
+  // assigning it a default value. This way we ensure that only the intended
+  // languages get a default value. Otherwise we could have default values for
+  // not yet open languages.
+  if (empty($object) || !property_exists($object, $field['field_name']) || 
+    (isset($object->{$field['field_name']}[$langcode]) && count($object->{$field['field_name']}[$langcode]) == 0)) {
+    $items = field_get_default_value($obj_type, $object, $field, $instance, $langcode);
   }
 }
+
 /**
  * Default field 'view' operation.
  *
  * @see field_attach_view()
  */
-function field_default_view($obj_type, $object, $field, $instance, $items, $build_mode) {
+function field_default_view($obj_type, $object, $field, $instance, $langcode, $items, $build_mode) {
   list($id, $vid, $bundle) = field_attach_extract_ids($obj_type, $object);
 
   $addition = array();
@@ -82,6 +88,7 @@ function field_default_view($obj_type, $
       '#label_display' => $label_display,
       '#build_mode' => $build_mode,
       '#single' => $single,
+      '#language' => $langcode,
       'items' => array(),
     );
 
@@ -117,7 +124,7 @@ function field_default_view($obj_type, $
   return $addition;
 }
 
-function field_default_prepare_translation($obj_type, $object, $field, $instance, &$items) {
+function field_default_prepare_translation($obj_type, $object, $field, $instance, $langcode, &$items) {
   $addition = array();
   if (isset($object->translation_source->$field['field_name'])) {
     $addition[$field['field_name']] = $object->translation_source->$field['field_name'];
Index: modules/field/field.form.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.form.inc,v
retrieving revision 1.15
diff -u -p -r1.15 field.form.inc
--- modules/field/field.form.inc	19 Aug 2009 13:31:12 -0000	1.15
+++ modules/field/field.form.inc	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 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Multilingual field API helper functions.
+ */
+
+/**
+ * Collect the available languages for the given entity type and field.
+ *
+ * If an entity has translation handler and the given field is translatable
+ * a (not necessarily strict) subset of the current enabled languages will be
+ * returned, otherwise only FIELD_LANGUAGE_NONE will be returned. This way,
+ * since the default value for a 'translatable' entity property is FALSE, we
+ * ensure that only entities able to handle translations actually get
+ * translatable fields.
+ *
+ * @param $obj_type
+ *   The type of the entity the field is attached to, e.g. 'node' or 'user'.
+ * @param $field
+ *   A field structure.
+ * @param $suggested_languages
+ *   An array of language preferences which will be intersected with the enabled
+ *   languages.
+ * @return
+ *   An array of valid language codes.
+ */
+function field_multilingual_available_languages($obj_type, $field, $suggested_languages = NULL) {
+  $field_languages = &drupal_static(__FUNCTION__, array());
+  $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']) {
+      $available_languages = field_multilingual_content_languages();
+      // The returned languages are a subset of the intersection of enabled ones
+      // and suggested ones.
+      $languages = !empty($suggested_languages) ? $available_languages = array_intersect($available_languages, $suggested_languages) : $available_languages;
+      foreach (module_implements('field_languages') as $module) {
+        $function = $module . '_field_languages';
+        $function($obj_type, $field, $languages);
+      }
+      // Accept only available languages.
+      $result = array_values(array_intersect($available_languages, $languages));
+      // Do not cache suggested values as they might alter the general result.
+      if (empty($suggested_languages)) {
+        $field_languages[$field_name] = $result;
+      }
+    }
+    else {
+      $result = $field_languages[$field_name] = array(FIELD_LANGUAGE_NONE);
+    }
+  }
+  else {
+    $result = $field_languages[$field_name];
+  }
+
+  return $result;
+}
+
+/**
+ * Return available content languages.
+ *
+ * The languages that may be associated to fields include FIELD_LANGAUGE_NONE.
+ *
+ * @return
+ *   An array of language codes.
+ */
+function field_multilingual_content_languages() {
+  return array_keys(language_list() + array(FIELD_LANGUAGE_NONE => 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.
