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	22 Feb 2009 23:46:52 -0000
@@ -359,3 +359,68 @@
     }
   }
 }
+
+/**
+ * @ingroup field
+ * @{
+ * Attach and process field translations to Drupal objects.
+ */
+
+/**
+ * Implementation of hook_field_attach_load().
+ *
+ * For multilingual fields, populate the value for each
+ * $object->field_name[$delta] 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 curent request. Field rendering and formatting is
+ * agnostic to whether a field is multilingual or not, however we leave the
+ * #languages array in situ to allow other modules to access it if necessary.
+ */
+function translation_field_attach_load($obj_type, $object) {
+  global $language;
+
+  // 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;
+  }
+
+  $default = language_default();
+  foreach ($fields as $field_name) {
+    foreach ($object->$field_name as $delta => $item) {
+      $best_fit = array();
+      // Use current language, if available.
+      if (isset($item['#languages'][$language->language])) {
+        $best_fit = $item['#languages'][$language->language];
+      }
+      // Use the source translation language.
+      elseif (isset($object->language_default) && isset($item['#languages'][$object->language_default])) {
+        $best_fit = $item['#languages'][$object->language_default];
+      }
+      // Use default language, if available.
+      elseif (isset($item['#languages'][$default->language])) {
+        $best_fit = $item['#languages'][$default->language];
+      }
+      // Use language neutral values, if available.
+      elseif (isset($item['#languages'][''])) {
+        $best_fit = $item['#languages'][''];
+      }
+            
+      $object->{$field_name}[$delta] = array_merge($item, $best_fit);
+    }
+  }
+}
+
+/**
+ * @} 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	22 Feb 2009 23:46:51 -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	22 Feb 2009 23:46:51 -0000
@@ -219,8 +219,10 @@
       $queried_objects[$id] = $objects[$id];
     }
   }
+  
   // Fetch other nodes from the database.
   if ($queried_objects) {
+    
     // 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.
@@ -230,7 +232,32 @@
         $queried_objects[$id]->$key = $value;
       }
     }
-
+    
+    // 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;
+      }
+    }
+    
     // 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 +279,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 +287,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);
+    }
+  }
 }
 
 /**
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	22 Feb 2009 23:46:51 -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	22 Feb 2009 23:46:52 -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,16 @@
           $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_name][$row->delta]['#languages'][$row->language] = $item;
+        }
+        else {
+          // 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 +272,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 +289,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 +299,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 +402,4 @@
       ->condition('bundle', $bundle_old)
       ->execute();
   }
-}
\ No newline at end of file
+}
