Index: modules/translation/translation.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/translation/translation.module,v
retrieving revision 1.39
diff -u -r1.39 translation.module
--- modules/translation/translation.module	25 Feb 2009 13:39:20 -0000	1.39
+++ modules/translation/translation.module	26 Feb 2009 23:45:28 -0000
@@ -359,3 +359,30 @@
     }
   }
 }
+
+/**
+ * @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) {
+  // TODO: Use field_attach_translate when field.autoload.inc gets properly updated.
+  _field_attach_translate($obj_type, $object);
+}
+
+/**
+ * @} 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	26 Feb 2009 23:45:27 -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	26 Feb 2009 23:45:26 -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,54 @@
 }
 
 /**
+ * Switch in the given entity the appropriate field translations
+ * according to the given language.
+ *
+ * @param $obj_type
+ *   The type of objects for which to load fields; e.g. 'node' or
+ *   'user'.
+ * @param $object
+ *   An object to be translated.
+ * @param $translation_language
+ *   The language the object has to be translated into.
+ * 
+ * @return TRUE if any translatable field was processed.
+ */
+function _field_attach_translate($obj_type, $object, $translation_language = NULL) {
+  
+  // 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;
+  }
+
+  // The default language is the current one.
+  if (empty($translation_language)) {
+    global $language;
+    $translation_language = isset($language->language) ? $language->language : '';
+  }
+
+  // Replace the value for the field with the translated value.
+  $info = field_info_fieldable_types($obj_type);
+  foreach ($fields as $field_name) {
+    if (isset($object->{$info['translations key']}[$translation_language]->{$field_name})) {
+      $object->{$field_name} = $object->{$info['translations key']}[$translation_language]->{$field_name};
+    }
+  }
+  
+  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.api.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.api.php,v
retrieving revision 1.2
diff -u -r1.2 field.api.php
--- modules/field/field.api.php	10 Feb 2009 03:16:14 -0000	1.2
+++ modules/field/field.api.php	26 Feb 2009 23:45:26 -0000
@@ -27,6 +27,8 @@
  *   bundle key: The object property that contains the bundle name for
  *     the object (bundle name is what nodes call "content type").
  *     The bundle name defines which fields are connected to the object.
+ *   translations key: The object property that contains the field
+ *     translations.
  *   cacheable: A boolean indicating whether Field API should cache
  *     loaded fields for each object, reducing the cost of
  *     field_attach_load().
@@ -41,6 +43,7 @@
       'id key' => 'nid',
       'revision key' => 'vid',
       'bundle key' => 'type',
+      'translations key' => 'translations',
       // Node.module handles its own caching.
       'cacheable' => FALSE,
       // Bundles must provide human readable name so
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	26 Feb 2009 23:45:27 -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/field.info.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.info.inc,v
retrieving revision 1.4
diff -u -r1.4 field.info.inc
--- modules/field/field.info.inc	10 Feb 2009 16:09:00 -0000	1.4
+++ modules/field/field.info.inc	26 Feb 2009 23:45:27 -0000
@@ -134,6 +134,12 @@
           if (empty($fieldable_info['bundle key'])) {
             $fieldable_info['bundles'] = array($name => $fieldable_info['name']);
           }
+          // There is no strong reason to expect that this property needs to 
+          // differ between object types, so we explicitly provide a sensible
+          // default value.
+          if (empty($fieldable_info['translations key'])) {
+            $fieldable_info['translations key'] = 'translations';
+          }
           $info['fieldable types'][$name] = $fieldable_info;
           $info['fieldable types'][$name]['module'] = $module;
         }
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	26 Feb 2009 23:45:27 -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,
@@ -186,6 +194,7 @@
 function field_sql_storage_field_storage_load($obj_type, $objects, $age) {
   $etid = _field_sql_storage_etid($obj_type);
   $load_current = $age == FIELD_LOAD_CURRENT;
+  $info = field_info_fieldable_types($obj_type);
 
   // Gather ids needed for each field.
   $field_ids = array();
@@ -202,7 +211,7 @@
   foreach ($field_ids as $field_name => $ids) {
     $field = field_info_field($field_name);
     $table = $load_current ? _field_sql_storage_tablename($field_name) : _field_sql_storage_revision_tablename($field_name);
-
+    
     $results = db_select($table, 't')
       ->fields('t')
       ->condition('etid', $etid)
@@ -212,7 +221,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 +229,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][$info['translations key']][$row->language]->{$field_name}[$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 +275,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 +292,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 +302,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 +405,4 @@
       ->condition('bundle', $bundle_old)
       ->execute();
   }
-}
\ No newline at end of file
+}
