Commit cfe7f8e6 authored by Dries's avatar Dries

- Patch #657972 by plach, Berdir: make field fallback logic actually reusable.

parent 85969b05
......@@ -1062,11 +1062,43 @@ function hook_field_attach_delete_revision($entity_type, $entity) {
* - obj_type: The type of $entity; e.g. 'node' or 'user'.
* - object: The entity with fields to render.
* - view_mode: View mode, e.g. 'full', 'teaser'...
* - langcode: The language in which the field values will be displayed.
*/
function hook_field_attach_view_alter(&$output, $context) {
}
/**
* Act on field_language().
*
* This hook is invoked to alter the array of display languages for the given
* entity.
*
* @param $display_language
* A reference to an array of language codes keyed by field name.
* @param $context
* An associative array containing:
* - entity_type: The type of the entity to be displayed.
* - entity: The entity with fields to render.
* - langcode: The language code $entity has to be displayed in.
*/
function hook_field_language_alter(&$display_language, $context) {
}
/**
* Act on field_available_languages().
*
* This hook is invoked to alter the array of available languages for the given
* field.
*
* @param &$languages
* A reference to an array of language codes to be made available.
* @param $context
* An associative array containing:
* - entity_type: The type of the entity the field is attached to.
* - field: A field data structure.
*/
function hook_field_available_languages_alter(&$languages, $context) {
}
/**
* Act on field_attach_create_bundle.
*
......
......@@ -168,6 +168,9 @@ class FieldQueryException extends FieldException {}
* - 'deleted': If TRUE, the function will operate on deleted fields
* as well as non-deleted fields. If unset or FALSE, only
* non-deleted fields are operated on.
* - 'language': A language code or an array of language codes keyed by field
* name. It will be used to narrow down to a single value the available
* languages to act on.
*/
function _field_invoke($op, $entity_type, $entity, &$a = NULL, &$b = NULL, $options = array()) {
// Merge default options.
......@@ -196,10 +199,14 @@ function _field_invoke($op, $entity_type, $entity, &$a = NULL, &$b = NULL, $opti
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']);
// Unless a language suggestion is provided we iterate on all the
// available languages.
$available_languages = field_available_languages($entity_type, $field);
$languages = _field_language_suggestion($available_languages, $options['language'], $field_name);
// Initialize field translations according to the available languages.
foreach (field_multilingual_available_languages($entity_type, $field, $suggested_languages) as $langcode) {
foreach ($languages as $langcode) {
$field_translations[$langcode] = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();
}
......@@ -271,6 +278,10 @@ function _field_invoke($op, $entity_type, $entity, &$a = NULL, &$b = NULL, $opti
* - 'deleted': If TRUE, the function will operate on deleted fields
* as well as non-deleted fields. If unset or FALSE, only
* non-deleted fields are operated on.
* - 'language': A language code or an array of language codes keyed by field
* name. It will be used to narrow down to a single value the available
* languages to act on.
*
* @return
* An array of returned values keyed by entity id.
*/
......@@ -321,8 +332,11 @@ function _field_invoke_multiple($op, $entity_type, $entities, &$a = NULL, &$b =
$grouped_entities[$field_id][$id] = $entities[$id];
// Extract the field values into a separate variable, easily accessed
// by hook implementations.
$suggested_languages = empty($options['language']) ? NULL : array($options['language']);
foreach (field_multilingual_available_languages($entity_type, $fields[$field_id], $suggested_languages) as $langcode) {
// Unless a language suggestion is provided we iterate on all the
// available languages.
$available_languages = field_available_languages($entity_type, $fields[$field_id]);
$languages = _field_language_suggestion($available_languages, $options['language'], $field_name);
foreach ($languages as $langcode) {
$grouped_items[$field_id][$langcode][$id] = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();
}
}
......@@ -492,7 +506,7 @@ function _field_invoke_multiple_default($op, $entity_type, $entities, &$a = NULL
*/
function field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode = NULL) {
// If no language is provided use the default site language.
$options = array('language' => field_multilingual_valid_language($langcode));
$options = array('language' => field_valid_language($langcode));
$form += (array) _field_invoke_default('form', $entity_type, $entity, $form, $form_state, $options);
// Add custom weight handling.
......@@ -1200,9 +1214,12 @@ function field_attach_prepare_view($entity_type, $entities, $view_mode = 'full')
* A renderable array for the field values.
*/
function field_attach_view($entity_type, $entity, $view_mode = 'full', $langcode = NULL) {
// Invoke field_default_view(). If no language is provided, use the current
// UI language.
$options = array('language' => field_multilingual_valid_language($langcode, FALSE));
// Determine the actual language to display for each field, given the
// languages available in the field data.
$display_language = field_language($entity_type, $entity, NULL, $langcode);
$options = array('language' => $display_language);
// Invoke field_default_view().
$null = NULL;
$output = _field_invoke_default('view', $entity_type, $entity, $view_mode, $null, $options);
......@@ -1219,7 +1236,6 @@ function field_attach_view($entity_type, $entity, $view_mode = 'full', $langcode
'obj_type' => $entity_type,
'object' => $entity,
'view_mode' => $view_mode,
'langcode' => $langcode,
);
drupal_alter('field_attach_view', $output, $context);
......
......@@ -496,10 +496,8 @@ function field_view_value($entity_type, $entity, $field_name, $item, $display =
$output = array();
if ($field = field_info_field($field_name)) {
$langcode = field_multilingual_valid_language($langcode, FALSE);
// Determine the langcode that will be used by language fallback.
$langcode = current(field_multilingual_available_languages($entity_type, $field, array($langcode)));
$langcode = field_language($entity_type, $entity, $field_name, $langcode);
// Push the item as the single value for the field, and defer to
// field_view_field() to build the render array for the whole field.
......@@ -588,7 +586,10 @@ function field_view_field($entity_type, $entity, $field_name, $display = array()
// Hook invocations are done through the _field_invoke() functions in
// 'single field' mode, to reuse the language fallback logic.
$options = array('field_name' => $field_name, 'language' => field_multilingual_valid_language($langcode, FALSE));
// Determine the actual language to display for the field, given the
// languages available in the field data.
$display_language = field_language($entity_type, $entity, $field_name, $langcode);
$options = array('field_name' => $field_name, 'language' => $display_language);
$null = NULL;
// Invoke prepare_view steps if needed.
......@@ -604,13 +605,12 @@ function field_view_field($entity_type, $entity, $field_name, $display = array()
// Build the renderable array.
$result = _field_invoke_default('view', $entity_type, $entity, $display, $null, $options);
// Invoke hook_field_attach_view_alter() to tet other modules alter the
// Invoke hook_field_attach_view_alter() to let other modules alter the
// renderable array, as in a full field_attach_view() execution.
$context = array(
'obj_type' => $entity_type,
'object' => $entity,
'view_mode' => '_custom',
'langcode' => $langcode,
);
drupal_alter('field_attach_view', $result, $context);
......@@ -623,6 +623,27 @@ function field_view_field($entity_type, $entity, $field_name, $display = array()
return $output;
}
/**
* Returns the field items in the language they currently would be displayed.
*
* @param $entity_type
* The type of $entity.
* @param $entity
* The entity containing the data to be displayed.
* @param $field_name
* The field to be displayed.
* @param $langcode
* (optional) The language code $entity->{$field_name} has to be displayed in.
* Defaults to the current language.
*
* @return
* An array of field items keyed by delta if available, FALSE otherwise.
*/
function field_get_items($entity_type, $entity, $field_name, $langcode = NULL) {
$langcode = field_language($entity_type, $entity, $field_name, $langcode);
return isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : FALSE;
}
/**
* Determine whether a field has any data.
*
......
This diff is collapsed.
......@@ -311,7 +311,7 @@ function field_sql_storage_field_storage_load($entity_type, $entities, $age, $fi
->fields('t')
->condition('etid', $etid)
->condition($load_current ? 'entity_id' : 'revision_id', $ids, 'IN')
->condition('language', field_multilingual_available_languages($entity_type, $field), 'IN')
->condition('language', field_available_languages($entity_type, $field), 'IN')
->orderBy('delta');
if (empty($options['deleted'])) {
......@@ -356,7 +356,7 @@ function field_sql_storage_field_storage_write($entity_type, $entity, $op, $fiel
$table_name = _field_sql_storage_tablename($field);
$revision_name = _field_sql_storage_revision_tablename($field);
$all_languages = field_multilingual_available_languages($entity_type, $field);
$all_languages = field_available_languages($entity_type, $field);
$field_languages = array_intersect($all_languages, array_keys((array) $entity->$field_name));
// Delete and insert, rather than update, in case a value was added.
......
This diff is collapsed.
......@@ -63,6 +63,19 @@ function field_test_entity_info_alter(&$entity_info) {
}
}
/**
* Helper function to enable entity translations.
*/
function field_test_entity_info_translatable($entity_type = NULL, $translatable = NULL) {
drupal_static_reset('field_has_translation_handler');
$stored_value = &drupal_static(__FUNCTION__, array());
if (isset($entity_type)) {
$stored_value[$entity_type] = $translatable;
entity_info_cache_clear();
}
return $stored_value;
}
/**
* Creates a new bundle for test_entity entities.
*
......
......@@ -87,27 +87,23 @@ function field_test_field_test_op_multiple($entity_type, $entities, $field, $ins
}
/**
* Implements hook_field_languages().
* Implements hook_field_available_languages_alter().
*/
function field_test_field_languages($entity_type, $field, &$languages) {
if ($field['settings']['test_hook_in']) {
function field_test_field_available_languages_alter(&$languages, $context) {
if (variable_get('field_test_field_available_languages_alter', FALSE)) {
// Add an unavailable language.
$languages[] = 'xx';
// Remove an available language.
unset($languages[0]);
$index = array_search('en', $languages);
unset($languages[$index]);
}
}
/**
* Helper function to enable entity translations.
* Implements hook_field_language_alter().
*/
function field_test_entity_info_translatable($entity_type = NULL, $translatable = NULL) {
$stored_value = &drupal_static(__FUNCTION__, array());
if (isset($entity_type)) {
$stored_value[$entity_type] = $translatable;
entity_info_cache_clear();
}
return $stored_value;
function field_test_field_language_alter(&$display_language, $context) {
locale_field_language_fallback($display_language, $context['entity'], $context['language']);
}
/**
......
......@@ -95,7 +95,7 @@ function field_test_field_storage_load($entity_type, $entities, $age, $fields, $
foreach ($field_data[$sub_table] as $row) {
if ($row->type == $entity_type && (!$row->deleted || $options['deleted'])) {
if (($load_current && in_array($row->entity_id, $ids)) || (!$load_current && in_array($row->revision_id, $ids))) {
if (in_array($row->language, field_multilingual_available_languages($entity_type, $field))) {
if (in_array($row->language, field_available_languages($entity_type, $field))) {
if (!isset($delta_count[$row->entity_id][$row->language])) {
$delta_count[$row->entity_id][$row->language] = 0;
}
......@@ -127,7 +127,7 @@ function field_test_field_storage_write($entity_type, $entity, $op, $fields) {
$field_name = $field['field_name'];
$field_data = &$data[$field_id];
$all_languages = field_multilingual_available_languages($entity_type, $field);
$all_languages = field_available_languages($entity_type, $field);
$field_languages = array_intersect($all_languages, array_keys((array) $entity->$field_name));
// Delete and insert, rather than update, in case a value was added.
......
<?php
// $Id$
/**
* @file
* Field API multilingual handling.
*/
/**
* Form submit handler for node_form().
*
* Update the field language according to the node language, changing the
* previous language if necessary.
*/
function locale_field_node_form_update_field_language($form, &$form_state, $reset_previous = TRUE) {
$node = (object) $form_state['values'];
$available_languages = field_multilingual_content_languages();
// @todo: Unify language neutral language codes.
$selected_language = empty($node->language) ? LANGUAGE_NONE : $node->language;
list(, , $bundle) = entity_extract_ids('node', $node);
foreach (field_info_instances('node', $bundle) as $instance) {
$field_name = $instance['field_name'];
$field = field_info_field($field_name);
$previous_language = $form[$field_name]['#language'];
// Handle a possible language change: previous language values are deleted,
// new ones are inserted.
if ($field['translatable'] && $previous_language != $selected_language) {
$form_state['values'][$field_name][$selected_language] = $node->{$field_name}[$previous_language];
if ($reset_previous) {
$form_state['values'][$field_name][$previous_language] = array();
}
}
}
}
/**
* Apply fallback rules to the given object.
*
* Parameters are the same of hook_field_attach_view().
*/
function locale_field_fallback_view(&$output, $context) {
// Lazily init fallback values and candidates to avoid unnecessary calls.
$fallback_values = array();
$fallback_candidates = NULL;
list(, , $bundle) = entity_extract_ids($context['obj_type'], $context['object']);
foreach (field_info_instances($context['obj_type'], $bundle) as $instance) {
$field_name = $instance['field_name'];
$field = field_info_field($field_name);
// If the items array is empty then we have a missing field translation.
// @todo: Verify this assumption.
if (isset($output[$field_name]) && count(element_children($output[$field_name])) == 0) {
if (!isset($fallback_candidates)) {
require_once DRUPAL_ROOT . '/includes/language.inc';
$fallback_candidates = language_fallback_get_candidates();
}
foreach ($fallback_candidates as $langcode) {
// Again if we have a non-empty array we assume the field translation is
// valid.
if (!empty($context['object']->{$field_name}[$langcode])) {
// Cache fallback values per language as fields might have different
// fallback values.
if (!isset($fallback_values[$langcode])) {
$fallback_values[$langcode] = field_attach_view($context['obj_type'], $context['object'], $context['view_mode'], $langcode);
}
// We are done, skip to the next field.
$output[$field_name] = $fallback_values[$langcode][$field_name];
break;
}
}
}
}
}
......@@ -6,7 +6,6 @@ version = VERSION
core = 7.x
files[] = locale.module
files[] = locale.install
files[] = locale.field.inc
files[] = locale.admin.inc
files[] = locale.test
configure = admin/config/regional/language
......@@ -113,6 +113,7 @@ function locale_uninstall() {
variable_del('locale_cache_strings');
variable_del('locale_js_directory');
variable_del('javascript_parsed');
variable_del('locale_field_language_fallback');
foreach (language_types() as $type) {
variable_del("language_negotiation_$type");
......
......@@ -410,13 +410,27 @@ function locale_form_alter(&$form, &$form_state, $form_id) {
/**
* Form submit handler for node_form().
*
* Check if Locale is registered as a translation handler and handle possible
* Checks if Locale is registered as a translation handler and handle possible
* node language changes.
*/
function locale_field_node_form_submit($form, &$form_state) {
if (field_multilingual_check_translation_handlers('node', 'locale')) {
module_load_include('inc', 'locale', 'locale.field');
locale_field_node_form_update_field_language($form, $form_state);
if (field_has_translation_handler('node', 'locale')) {
$node = (object) $form_state['values'];
$available_languages = field_content_languages();
list(, , $bundle) = entity_extract_ids('node', $node);
foreach (field_info_instances('node', $bundle) as $instance) {
$field_name = $instance['field_name'];
$field = field_info_field($field_name);
$previous_language = $form[$field_name]['#language'];
// Handle a possible language change: new language values are inserted,
// previous ones are deleted.
if ($field['translatable'] && $previous_language != $node->language) {
$form_state['values'][$field_name][$node->language] = $node->{$field_name}[$previous_language];
$form_state['values'][$field_name][$previous_language] = array();
}
}
}
}
......@@ -438,20 +452,57 @@ function locale_theme() {
}
/**
* Implements hook_field_attach_view_alter().
* Implements hook_field_language_alter().
*/
function locale_field_attach_view_alter(&$output, $context) {
// In locale_field_fallback_view() we might call field_attach_view(). The
// static variable avoids unnecessary recursion.
static $recursion;
function locale_field_language_alter(&$display_language, $context) {
// Do not apply core language fallback rules if they are disabled or if Locale
// is not registered as a translation handler.
if (variable_get('locale_field_language_fallback', TRUE) && field_has_translation_handler($context['entity_type'], 'locale')) {
locale_field_language_fallback($display_language, $context['entity'], $context['language']);
}
}
// Do not apply fallback rules if disabled or if Locale is not registered as a
// translation handler.
if (!$recursion && variable_get('locale_field_fallback_view', TRUE) && field_multilingual_check_translation_handlers($context['obj_type'], 'locale')) {
$recursion = TRUE;
module_load_include('inc', 'locale', 'locale.field');
locale_field_fallback_view($output, $context);
$recursion = FALSE;
/**
* Applies language fallback rules to the fields attached to the given entity.
*
* Core language fallback rules simply check if fields have a field translation
* for the requested language code. If so the requested language is returned,
* otherwise all the fallback candidates are inspected to see if there is a
* field translation available in another language.
* By default this is called by locale_field_language_alter(), but this
* behavior can be disabled by setting the 'locale_field_language_fallback'
* variable to FALSE.
*
* @param $display_language
* A reference to an array of language codes keyed by field name.
* @param $entity
* The entity to be displayed.
* @param $langcode
* The language code $entity has to be displayed in.
*/
function locale_field_language_fallback(&$display_language, $entity, $langcode) {
// Lazily init fallback candidates to avoid unnecessary calls.
$fallback_candidates = NULL;
$field_languages = array();
foreach ($display_language as $field_name => $field_language) {
// If the requested language is defined for the current field use it,
// otherwise search for a fallback value among the fallback candidates.
if (isset($entity->{$field_name}[$langcode])) {
$display_language[$field_name] = $langcode;
}
elseif (!empty($entity->{$field_name})) {
if (!isset($fallback_candidates)) {
require_once DRUPAL_ROOT . '/includes/language.inc';
$fallback_candidates = language_fallback_get_candidates();
}
foreach ($fallback_candidates as $fallback_language) {
if (isset($entity->{$field_name}[$fallback_language])) {
$display_language[$field_name] = $fallback_language;
break;
}
}
}
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment