Index: modules/field/field.autoload.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.autoload.inc,v retrieving revision 1.5 diff -u -r1.5 field.autoload.inc --- modules/field/field.autoload.inc 8 Mar 2009 04:25:04 -0000 1.5 +++ modules/field/field.autoload.inc 15 Mar 2009 23:24:29 -0000 @@ -624,3 +624,13 @@ /** * @} End of "field_info" */ + + +/* + * The following definitions will be overridden when the file gets rebuilt. + */ + +function field_attach_translate($obj_type, $object, $translation_language = NULL) { + require_once DRUPAL_ROOT . '/modules/field/field.attach.inc'; + return _field_attach_translate($obj_type, $object, $translation_language); +} Index: modules/field/field.install =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.install,v retrieving revision 1.4 diff -u -r1.4 field.install --- modules/field/field.install 10 Mar 2009 09:45:31 -0000 1.4 +++ modules/field/field.install 15 Mar 2009 23:24:30 -0000 @@ -58,6 +58,12 @@ 'not null' => TRUE, 'default' => 0, ), + 'translatable' => array( + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 0, + ), 'active' => array( 'type' => 'int', 'size' => 'tiny', @@ -135,6 +141,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.default.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.default.inc,v retrieving revision 1.4 diff -u -r1.4 field.default.inc --- modules/field/field.default.inc 8 Mar 2009 04:25:04 -0000 1.4 +++ modules/field/field.default.inc 15 Mar 2009 23:24:29 -0000 @@ -39,6 +39,18 @@ // Filter out empty values. $items = field_set_empty($field, $items); + // If the object is being created, the given language is the default language. + $language = isset($form_state['values']['language']) ? $form_state['values']['language'] : ''; + $info = field_info_fieldable_types($obj_type); + if (empty($object->{$info['id key']})) { + $object->{$info['language default key']} = $language; + } + + if ($field['translatable']) { + // Save the field translation. + $object->{$info['translations key']}[$language]->{$field['field_name']} = $items; + } + // _field_invoke() does not add back items for fields not present in the // original $object, so add them manually. $object->{$field['field_name']} = $items; Index: modules/field/field.attach.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.attach.inc,v retrieving revision 1.5 diff -u -r1.5 field.attach.inc --- modules/field/field.attach.inc 13 Mar 2009 21:25:40 -0000 1.5 +++ modules/field/field.attach.inc 15 Mar 2009 23:24:28 -0000 @@ -233,18 +233,51 @@ $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) { + $info = field_info_fieldable_types($obj_type); + $result = db_select('field_object_language') + ->fields(NULL, array('id', '`default`')) + ->condition('type', $obj_type) + ->condition('id', array_keys($queried_objects), 'IN')->execute(); + foreach ($result as $row) { + $queried_objects[$row->id]->{$info['language default key']} = $row->default; + $additions[$row->id][$info['language default key']] = $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. @@ -266,15 +299,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"; @@ -283,6 +307,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); + } + } } /** @@ -399,6 +431,7 @@ $function($obj_type, $object); } + _field_attach_save_object_language_default($obj_type, $object); _field_invoke('insert', $obj_type, $object); module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_write', $obj_type, $object, FIELD_STORAGE_INSERT); @@ -409,6 +442,29 @@ } /** + * Save the object default (source) language. + * + * @param $obj_type + * The type of $object; e.g. 'node' or 'user'. + * @param $object + * The object whose language has to be saved. + * @param + * The object default language. + */ +function _field_attach_save_object_language_default($obj_type, $object, $language = NULL) { + $info = field_info_fieldable_types($obj_type); + + if (!isset($language) && isset($object->{$info['language default key']})) { + $language = $object->{$info['language default key']}; + } + + db_insert('field_object_language') + ->fields(array('type', 'id', '`default`')) + ->values(array($obj_type, $object->{$info['id key']}, !empty($language) ? $language : '')) + ->execute(); +} + +/** * Save field data for an existing object. * * @param $obj_type @@ -446,13 +502,16 @@ _field_invoke('delete', $obj_type, $object); module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_delete', $obj_type, $object); + // Delete the default object language setting. + list($id, $vid, $bundle, $cacheable) = field_attach_extract_ids($obj_type, $object); + db_delete('field_object_language')->condition('type', $obj_type)->condition('id', $id)->execute(); + // Let other modules act on deleting the object. foreach (module_implements('field_attach_delete') as $module) { $function = $module . '_field_attach_delete'; $function($obj_type, $object); } - list($id, $vid, $bundle, $cacheable) = field_attach_extract_ids($obj_type, $object); if ($cacheable) { cache_clear_all("field:$obj_type:$id:", 'cache_field', TRUE); } @@ -546,6 +605,53 @@ } /** + * 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 = !empty($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.3 diff -u -r1.3 field.api.php --- modules/field/field.api.php 13 Mar 2009 21:25:40 -0000 1.3 +++ modules/field/field.api.php 15 Mar 2009 23:24:28 -0000 @@ -27,6 +27,10 @@ * 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. + * language default key: The object property that contains the object + * default language translation. * cacheable: A boolean indicating whether Field API should cache * loaded fields for each object, reducing the cost of * field_attach_load(). @@ -41,6 +45,8 @@ 'id key' => 'nid', 'revision key' => 'vid', 'bundle key' => 'type', + 'translations key' => 'translations', + 'language default key' => 'language_default', // 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.6 diff -u -r1.6 field.crud.inc --- modules/field/field.crud.inc 13 Mar 2009 14:34:58 -0000 1.6 +++ modules/field/field.crud.inc 15 Mar 2009 23:24:29 -0000 @@ -51,6 +51,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) @@ -205,6 +207,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 15 Mar 2009 23:24:29 -0000 @@ -134,6 +134,15 @@ if (empty($fieldable_info['bundle key'])) { $fieldable_info['bundles'] = array($name => $fieldable_info['name']); } + // There is no strong reason to expect that these properties need to + // differ between object types, so we explicitly provide a sensible + // default value. + if (empty($fieldable_info['translations key'])) { + $fieldable_info['translations key'] = 'translations'; + } + if (empty($fieldable_info['language default key'])) { + $fieldable_info['language default key'] = 'language_default'; + } $info['fieldable types'][$name] = $fieldable_info; $info['fieldable types'][$name]['module'] = $module; } Index: modules/field/field.test =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.test,v retrieving revision 1.5 diff -u -r1.5 field.test --- modules/field/field.test 10 Feb 2009 03:16:14 -0000 1.5 +++ modules/field/field.test 15 Mar 2009 23:24:31 -0000 @@ -184,20 +184,22 @@ // most part, these tests pass by not crashing or causing exceptions. function testFieldAttachSaveMissingData() { $entity_type = 'test_entity'; - $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); // Insert: Field is missing + $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']); field_attach_insert($entity_type, $entity); $count = db_result(db_query("SELECT COUNT(*) FROM {{$this->table}}")); $this->assertEqual($count, 0, 'Missing field results in no inserts'); // Insert: Field is NULL + $entity = field_test_create_stub_entity(1, 1, $this->instance['bundle']); $entity->{$this->field_name} = NULL; field_attach_insert($entity_type, $entity); $count = db_result(db_query("SELECT COUNT(*) FROM {{$this->table}}")); $this->assertEqual($count, 0, 'NULL field results in no inserts'); // Add some real data + $entity = field_test_create_stub_entity(2, 2, $this->instance['bundle']); $entity->{$this->field_name} = array(0 => array('value' => 1)); field_attach_insert($entity_type, $entity); $count = db_result(db_query("SELECT COUNT(*) FROM {{$this->table}}")); Index: modules/translation/translation.module =================================================================== RCS file: /cvs/drupal/drupal/modules/translation/translation.module,v retrieving revision 1.40 diff -u -r1.40 translation.module --- modules/translation/translation.module 8 Mar 2009 04:25:07 -0000 1.40 +++ modules/translation/translation.module 15 Mar 2009 23:24:31 -0000 @@ -359,3 +359,29 @@ } } } + +/** + * @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) { + field_attach_translate($obj_type, $object); +} + +/** + * @} End of "ingroup field". + */ + 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.5 diff -u -r1.5 field_sql_storage.module --- modules/field/modules/field_sql_storage/field_sql_storage.module 13 Mar 2009 21:25:40 -0000 1.5 +++ modules/field/modules/field_sql_storage/field_sql_storage.module 15 Mar 2009 23:24:31 -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,7 +194,8 @@ 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(); $delta_count = 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,19 @@ $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 dedicated 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]->{$info['language default key']}) && + $objects[$row->entity_id]->{$info['language default key']} == $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]++; } } @@ -230,15 +250,17 @@ } function field_sql_storage_field_storage_write($obj_type, $object, $op) { - list($id, $vid, $bundle) = field_attach_extract_ids($obj_type, $object); - $etid = _field_sql_storage_etid($obj_type); + $info = new stdClass(); + list($info->id, $info->vid, $info->bundle) = field_attach_extract_ids($obj_type, $object); + $info->etid = _field_sql_storage_etid($obj_type); + $fieldable_info = field_info_fieldable_types($obj_type); - $instances = field_info_instances($bundle); + $instances = field_info_instances($info->bundle); foreach ($instances as $instance) { $field_name = $instance['field_name']; - $table_name = _field_sql_storage_tablename($field_name); - $revision_name = _field_sql_storage_revision_tablename($field_name); - $field = field_read_field($field_name); + $info->table_name = _field_sql_storage_tablename($field_name); + $info->revision_name = _field_sql_storage_revision_tablename($field_name); + $info->field = field_read_field($field_name); // Leave the field untouched if $object comes with no $field_name property. // Empty the field if $object->$field_name is NULL or an empty array. @@ -246,54 +268,80 @@ // 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)) { - // 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 (isset($vid)) { - db_delete($revision_name)->condition('etid', $etid)->condition('entity_id', $id)->condition('revision_id', $vid)->execute(); + $update = $op == FIELD_STORAGE_UPDATE; + if ($info->field['translatable']) { + foreach ($object->{$fieldable_info['translations key']} as $language => $translation) { + _field_sql_storage_field_storage_write_items($translation->$field_name, $info, $update, $language); } } + else { + _field_sql_storage_field_storage_write_items($object->$field_name, $info, $update); + } + } + } +} - if ($object->$field_name) { - // Prepare the multi-insert query. - $columns = array('etid', 'entity_id', 'revision_id', 'bundle', 'delta'); - foreach ($field['columns'] as $column => $attributes) { - $columns[] = _field_sql_storage_columnname($field_name, $column); - } - $query = db_insert($table_name)->fields($columns); - if (isset($vid)) { - $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; - } - } +/** + * Actually store the given field values with respect to the given language. + */ +function _field_sql_storage_field_storage_write_items($items, $info, $update, $language = NULL) { + // Delete and insert, rather than update, in case a value was added. + if ($update) { + $query = db_delete($info->table_name)->condition('etid', $info->etid)->condition('entity_id', $info->id); + if (isset($language)) { + $query->condition('language', $language); + } + $query->execute(); + if (isset($info->vid)) { + $revision_query = db_delete($info->revision_name)->condition('etid', $info->etid)->condition('entity_id', $info->id)->condition('revision_id', $info->vid); + if (isset($language)) { + $revision_query->condition('language', $language); + } + $revision_query->execute(); + } + } - // Execute the insert. - $query->execute(); - if (isset($vid)) { - $revision_query->execute(); - } + + if ($items) { + // Prepare the multi-insert query. + $columns = array('etid', 'entity_id', 'revision_id', 'bundle', 'delta', 'language'); + foreach ($info->field['columns'] as $column => $attributes) { + $columns[] = _field_sql_storage_columnname($info->field['field_name'], $column); + } + $query = db_insert($info->table_name)->fields($columns); + if (isset($info->vid)) { + $revision_query = db_insert($info->revision_name)->fields($columns); + } + + $delta_count = 0; + foreach ($items as $delta => $item) { + $record = array( + 'etid' => $info->etid, + 'entity_id' => $info->id, + 'revision_id' => $info->vid, + 'bundle' => $info->bundle, + 'delta' => $delta, + 'language' => !empty($language) ? $language : '', + ); + foreach ($info->field['columns'] as $column => $attributes) { + $record[_field_sql_storage_columnname($info->field['field_name'], $column)] = isset($item[$column]) ? $item[$column] : NULL; + } + $query->values($record); + if (isset($info->vid)) { + $revision_query->values($record); + } + + if ($info->field['cardinality'] != FIELD_CARDINALITY_UNLIMITED && empty($info->field['translatable']) && ++$delta_count == $info->field['cardinality']) { + break; } } + + // Execute the insert. + $query->execute(); + if (isset($info->vid)) { + $revision_query->execute(); + } } } @@ -385,4 +433,4 @@ ->condition('bundle', $bundle_old) ->execute(); } -} \ No newline at end of file +}