Index: modules/translation/translation.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/translation/translation.module,v
retrieving revision 1.38
diff -u -r1.38 translation.module
--- modules/translation/translation.module	22 Jan 2009 03:11:54 -0000	1.38
+++ modules/translation/translation.module	24 Feb 2009 00:25:43 -0000
@@ -359,3 +359,45 @@
     }
   }
 }
+
+/**
+ * @ingroup field
+ * @{
+ * Attach and process field translations to Drupal objects.
+ */
+
+/**
+ * Implementation of hook_field_attach_load().
+ *
+ * For multilingual fields, populate the value for $object->field_name with
+ * the best match for the current language or the site default. Since
+ * hook_field_attach_load() is not persistently cached, we can add language
+ * specific information here in the knowledge it will only affect the current
+ * request. Field rendering and formatting is agnostic to whether a field is
+ * multilingual or not, however we leave the field_translations array in situ
+ * to allow other modules to access it if necessary.
+ */
+function translation_field_attach_load($obj_type, $object) {
+  $language_preferences = array();
+
+  // Use current language if available.
+  global $language;
+  $language_preferences[] = $language->language;
+  
+  // Use the site default language if available.
+  $default = language_default();
+  $language_preferences[] = $default->language;
+  
+  // Use the default language for the object if available.
+  if (isset($object->language_default)) {
+    $language_preferences[] = $object->language_default;
+  }
+
+  // TODO: Use field_attach_switch_translation when field.autoload.inc gets properly updated.
+  _field_attach_switch_translation($obj_type, $object, $language_preferences);
+}
+
+/**
+ * @} End of "ingroup field".
+ */
+
Index: modules/field/field.install
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.install,v
retrieving revision 1.3
diff -u -r1.3 field.install
--- modules/field/field.install	11 Feb 2009 04:45:57 -0000	1.3
+++ modules/field/field.install	24 Feb 2009 00:25:42 -0000
@@ -53,6 +53,12 @@
         'not null' => TRUE,
           'default' => 0,
       ),
+      'translatable' => array(
+        'type' => 'int',
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
       'active' => array(
         'type' => 'int',
         'size' => 'tiny',
@@ -116,6 +122,21 @@
       'widget_type' => array('widget_type'),
     ),
   );
+
+  $schema['field_object_language'] = array(
+    'fields' => array(
+      'type' => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE,
+        'description' => 'The name of a translatable object.'),
+      'id' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0,
+        'description' => 'The id of the translatable object.',
+      ),
+      'default' => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE,
+        'description' => 'The source language of the translatable object.',
+      ),
+    ),
+    'primary key' => array('type', 'id'),
+  );
+
   $schema['cache_field'] = drupal_get_schema_unprocessed('system', 'cache');
 
   return $schema;
Index: modules/field/field.attach.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.attach.inc,v
retrieving revision 1.3
diff -u -r1.3 field.attach.inc
--- modules/field/field.attach.inc	10 Feb 2009 03:16:14 -0000	1.3
+++ modules/field/field.attach.inc	24 Feb 2009 00:25:42 -0000
@@ -219,18 +219,47 @@
       $queried_objects[$id] = $objects[$id];
     }
   }
+
   // Fetch other nodes from the database.
   if ($queried_objects) {
+    $additions = array();
+
+    // Fetch source language for translatable objects.
+    // @todo Performance optimization; cache field_info_instances() per bundle.
+    $translatable = FALSE;
+    foreach ($queried_objects as $id => $object) {
+      list($id, $vid, $bundle, $cacheable) = field_attach_extract_ids($obj_type, $object);
+      // Check if any translatable field is defined for the current object.
+      $instances = field_info_instances($bundle);
+      foreach ($instances as $instance) {
+        $field = field_info_field($instance['field_name']);
+        if ($field['translatable']) {
+          $translatable = TRUE;
+          break;
+        }
+      }
+    }
+
+    // Load the default language and store it as an addition in order to cache it.
+    if ($translatable) {
+      $result = db_query("SELECT id, `default` FROM {field_object_language} WHERE type = :type AND id IN (:ids)", array(':type' => $obj_type, ':ids' => array_keys($queried_objects)));
+      foreach ($result as $row) {
+        $queried_objects[$row->id]->language_default = $row->default;
+        $additions[$row->id]['language_default'] = $row->default;
+      }
+    }
+
     // We need the raw additions to be able to cache them, so
     // content_storage_load() and hook_field_load() must not alter
     // nodes directly but return their additions.
-    $additions = module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_load', $obj_type, $queried_objects, $age);
-    foreach ($additions as $id => $obj_additions) {
+    $storage_additions = module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_load', $obj_type, $queried_objects, $age);
+    foreach ($storage_additions as $id => $obj_additions) {
       foreach ($obj_additions as $key => $value) {
         $queried_objects[$id]->$key = $value;
+        $additions[$id][$key] = $value; 
       }
     }
-
+    
     // TODO D7 : to be consistent we might want to make hook_field_load() accept
     // multiple objects too. Which forbids going through _field_invoke(), but
     // requires manually iterating the instances instead.
@@ -252,15 +281,6 @@
         $additions[$id][$key] = $value;
       }
 
-      // Let other modules act on loading the object.
-      // TODO : this currently doesn't get cached (we cache $additions).
-      // This should either be called after we fetch from cache, or return an
-      // array of additions.
-      foreach (module_implements('field_attach_load') as $module) {
-        $function = $module . '_field_attach_load';
-        $function($obj_type, $queried_objects[$id]);
-      }
-
       // Cache the data.
       if ($cacheable) {
         $cid = "field:$obj_type:$id:$vid";
@@ -269,6 +289,14 @@
       }
     }
   }
+
+  // Let other modules act on loading the objects.
+  foreach ($objects as $object) {
+    foreach (module_implements('field_attach_load') as $module) {
+      $function = $module . '_field_attach_load';
+      $function($obj_type, $object);
+    }
+  }
 }
 
 /**
@@ -532,6 +560,63 @@
 }
 
 /**
+ * Switch in the given entity the appropriate field translations
+ * according to the passed language preferences.
+ *
+ * @param $obj_type
+ *   The type of objects for which to load fields; e.g. 'node' or
+ *   'user'.
+ * @param $objects
+ *   An array of objects for which to load fields. The keys for
+ *   primary id and bundle name to load are identified by
+ *   hook_fieldable_info for $obj_type.
+ * @param $language_choices
+ *   An array of language preferences: the first one with a field
+ *   translation available will be used for all the transatable
+ *   fields.
+ * 
+ * @return TRUE if any translatable field was processed.
+ */
+function _field_attach_switch_translation($obj_type, $object, $language_preferences) {
+  // Only operate on translatable fields.
+  $fields = array();
+  list(, , $bundle) = field_attach_extract_ids($obj_type, $object);
+  $instances = field_info_instances($bundle);
+  foreach ($instances as $instance) {
+    $field = field_info_field($instance['field_name']);
+    if (!empty($field['translatable'])) {
+      $fields[] = $instance['field_name'];
+    }
+  }
+  if (empty($fields)) {
+    return FALSE;
+  }
+
+  $language = NULL;
+  foreach ($fields as $field_name) {
+    // Lazy init: we assume that if an entity field is available in
+    // a language, then all the other fields will.
+    if (!isset($language)) {
+      foreach ($language_preferences as $language_preference) {
+        if (isset($object->field_translations[$field_name][$language_preference])) {
+          $language = $language_preference;
+          break;
+        }
+      }
+      // Do not override the default values if no better value is available.
+      if (!isset($language)) {
+        return FALSE;
+      }
+    }
+    // Replace the  value for the field name with the best fit translation.
+    $object->{$field_name} = $object->field_translations[$field_name][$language];
+  }
+  
+  return TRUE;
+}
+
+
+/**
  * Notify field.module that a new bundle was created.
  *
  * The default SQL-based storage doesn't need to do anytrhing about it, but
Index: modules/field/field.crud.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.crud.inc,v
retrieving revision 1.4
diff -u -r1.4 field.crud.inc
--- modules/field/field.crud.inc	10 Feb 2009 03:16:14 -0000	1.4
+++ modules/field/field.crud.inc	24 Feb 2009 00:25:42 -0000
@@ -48,6 +48,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)
@@ -195,6 +197,7 @@
 
   $field += array(
     'cardinality' => 1,
+    'translatable' => 0,
     'locked' => FALSE,
     'settings' => array(),
   );
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.4
diff -u -r1.4 field_sql_storage.module
--- modules/field/modules/field_sql_storage/field_sql_storage.module	10 Feb 2009 03:16:14 -0000	1.4
+++ modules/field/modules/field_sql_storage/field_sql_storage.module	24 Feb 2009 00:25:42 -0000
@@ -128,9 +128,17 @@
         'not null' => TRUE,
         'description' => 'The sequence number for this data item, used for multi-value fields',
       ),
+      // @todo Consider an integer field for 'language'.
+      'language' => array(
+        'type' => 'varchar',
+        'length' => 32,
+        'not null' => TRUE,
+        'default' => '',
+        'description' => 'The language for this data item.',
+      ),
     ),
-    'primary key' => array('etid', 'entity_id', 'deleted', 'delta'),
-    // TODO : index on 'bundle'
+    'primary key' => array('etid', 'entity_id', 'deleted', 'delta', 'language'),
+    // @todo Index on 'bundle'.
   );
 
   // Add field columns.
@@ -144,7 +152,7 @@
   $revision = $current;
   $revision['description'] = 'Revision archive storage for field ' . $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['field_name']) => $current,
@@ -212,7 +220,7 @@
       ->execute();
 
     foreach ($results as $row) {
-      if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta_count[$row->entity_id][$field_name] < $field['cardinality']) {
+      if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || !empty($field['translatable']) || $delta_count[$row->entity_id][$field_name] < $field['cardinality']) {
         $item = array();
         // For each column declared by the field, populate the item
         // from the prefixed database column.
@@ -220,8 +228,18 @@
           $item[$column] = $row->{_field_sql_storage_columnname($field_name, $column)};
         }
 
-        // Add the item to the field values for the entity.
-        $additions[$row->entity_id][$field_name][] = $item;
+        // For translatable fields, put the multilingual values for each delta
+        // into a '#languages' property. This information can be safely cached
+        // in the field and object caches.
+        if ($field['translatable']) {
+          $additions[$row->entity_id]['field_translations'][$field_name][$row->language][$row->delta] = $item;
+        }
+        // Load translation defaults according to the entity default language.
+        if (!$field['translatable'] || 
+            (isset($objects[$row->entity_id]->language_default) && $objects[$row->entity_id]->language_default == $row->language)) {
+          // Add the item to the field values for the entity.
+          $additions[$row->entity_id][$field_name][] = $item;
+        }
         $delta_count[$row->entity_id][$field_name]++;
       }
     }
@@ -256,7 +274,7 @@
 
       if ($object->$field_name) {
         // 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);
         }
@@ -273,6 +291,7 @@
             'revision_id' => $vid,
             'bundle' => $bundle,
             'delta' => $delta,
+            'language' => !empty($item->language) ? $item->language : '',
           );
           foreach ($field['columns'] as $column => $attributes) {
             $record[_field_sql_storage_columnname($field_name, $column)] = isset($item[$column]) ? $item[$column] : NULL;
@@ -282,7 +301,7 @@
             $revision_query->values($record);
           }
 
-          if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED && ++$delta_count == $field['cardinality']) {
+          if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED && empty($field['translatable']) && ++$delta_count == $field['cardinality']) {
             break;
           }
         }
@@ -385,4 +404,4 @@
       ->condition('bundle', $bundle_old)
       ->execute();
   }
-}
\ No newline at end of file
+}
