$entity_info) { if ($entity_info['fieldable']) { foreach ($entity_info['bundles'] as $bundle_name => $bundle_info) { if (isset($bundle_info['admin'])) { // Extract path information from the bundle. $path = $bundle_info['admin']['path']; // Different bundles can appear on the same path (e.g. %node_type and // %comment_node_type). To allow field_ui_menu_load() to extract the // actual bundle object from the translated menu router path // arguments, we need to identify the argument position of the bundle // name string ('bundle argument') and pass that position to the menu // loader. The position needs to be casted into a string; otherwise it // would be replaced with the bundle name string. if (isset($bundle_info['admin']['bundle argument'])) { $bundle_arg = $bundle_info['admin']['bundle argument']; $bundle_pos = (string) $bundle_arg; } else { $bundle_arg = $bundle_name; $bundle_pos = '0'; } // This is the position of the %field_ui_menu placeholder in the // items below. $field_position = count(explode('/', $path)) + 1; // Extract access information, providing defaults. $access = array_intersect_key($bundle_info['admin'], drupal_map_assoc(array('access callback', 'access arguments'))); $access += array( 'access callback' => 'user_access', 'access arguments' => array('administer site configuration'), ); $items["$path/fields/%field_ui_menu/translate"] = array( 'load arguments' => array($entity_type, $bundle_arg, $bundle_pos, '%map'), 'title' => 'Translate', 'page callback' => 'i18n_field_page_translate', 'page arguments' => array($field_position), 'file' => 'i18n_field.pages.inc', 'type' => MENU_LOCAL_TASK, ) + $access; $items["$path/fields/%field_ui_menu/translate/%i18n_language"] = array( 'load arguments' => array($entity_type, $bundle_arg, $bundle_pos, '%map'), 'title' => 'Instance', 'page callback' => 'i18n_field_page_translate', 'page arguments' => array($field_position, $field_position + 2), 'file' => 'i18n_field.pages.inc', 'type' => MENU_CALLBACK, ) + $access; } } } } return $items; } /** * Implements hook_hook_info(). */ function i18n_field_hook_info() { $hooks['i18n_field_info'] = array( 'group' => 'i18n', ); return $hooks; } /** * Implements hook_field_attach_form(). * * After the form fields are built. Translate title and description for fields with multiple values. */ function i18n_field_field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode) { // Determine the list of instances to iterate on. list(, , $bundle) = entity_extract_ids($entity_type, $entity); $instances = field_info_instances($entity_type, $bundle); foreach ($instances as $field_name => $instance) { if (isset($form[$field_name])) { $langcode = $form[$field_name]['#language']; $field = &$form[$field_name]; // Note: cardinality for unlimited fields is -1 if (isset($field[$langcode]['#cardinality']) && $field[$langcode]['#cardinality'] != 1) { $translated = i18n_string_object_translate('field_instance', $instance); if (!empty($field[$langcode]['#title'])) { $field[$langcode]['#title'] = $translated['label']; } if (!empty($field[$langcode]['#description'])) { $field[$langcode]['#description'] = $translated['description']; } } } } } /** * Implements hook_field_formatter_info(). */ function i18n_field_field_formatter_info() { $types = array(); foreach (i18n_field_type_info() as $type => $info) { if (!empty($info['translate_options'])) { $types[] = $type; } } return array( 'i18n_list_default' => array( 'label' => t('Default translated'), 'field types' => $types, ), ); } /** * Implements hook_field_formatter_view(). */ function i18n_field_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) { $element = array(); switch ($display['type']) { case 'i18n_list_default': if (($translate = i18n_field_type_info($field['type'], 'translate_options'))) { $allowed_values = $translate($field); } else { // Defaults to list_default behavior $allowed_values = list_allowed_values($field); } foreach ($items as $delta => $item) { if (isset($allowed_values[$item['value']])) { $output = field_filter_xss($allowed_values[$item['value']]); } else { // If no match was found in allowed values, fall back to the key. $output = field_filter_xss($item['value']); } $element[$delta] = array('#markup' => $output); } break; } return $element; } /** * Implements hook_field_widget_form_alter(). * * Translate: * - Title (label) * - Description (help) * - Default value * - List options */ function i18n_field_field_widget_form_alter(&$element, &$form_state, $context) { global $language; // Don't translate if the widget is being shown on the field edit form. if ($form_state['build_info']['form_id'] == 'field_ui_field_edit_form') { return; } // Skip if we are missing any of the parameters if (empty($context['field']) || empty($context['instance']) || empty($context['langcode'])) { return; } $field = $context['field']; $instance = $context['instance']; $langcode = $context['langcode']; // Get the element to alter. Account for inconsistencies in how the element // is built for different field types. if (isset($element[0]) && count($element) == 1) { // Single-value file fields and image fields. $alter_element = &$element[0]; } elseif ($field['type'] == 'url' && $field['module'] == 'url' && $field['cardinality'] == 1) { $alter_element = &$element; } elseif (isset($element['value'])) { // Number fields. Single-value text fields. $alter_element = &$element['value']; } elseif ($field['type'] == 'entityreference' && isset($element['target_id'])) { // Entityreference fields using the entityreference_autocomplete widget. $alter_element = &$element['target_id']; } elseif ($field['type'] == 'node_reference' && isset($element['nid'])) { // The node_reference fields using the entityreference_autocomplete widget. $alter_element = &$element['nid']; } else { // All other fields. $alter_element = &$element; } // If a subelement has the same title as the parent, translate it instead. // Allows fields such as email and commerce_price to be translated. foreach (element_get_visible_children($element) as $key) { $single_value = ($field['cardinality'] == 1); $has_title = (isset($element['#title']) && isset($element[$key]['#title'])); if ($single_value && $has_title && $element[$key]['#title'] == $element['#title']) { $alter_element = &$element[$key]; break; } } // The field language may affect some variables (default) but not others (description will be in current page language) $i18n_langcode = empty($alter_element['#language']) || $alter_element['#language'] == LANGUAGE_NONE ? $language->language : $alter_element['#language']; // Translate instance to current page language and set to form_state // so it will be used for validation messages later. $instance_current = i18n_string_object_translate('field_instance', $instance); if (isset($form_state['field'][$instance['field_name']][$langcode]['instance'])) { $form_state['field'][$instance['field_name']][$langcode]['instance'] = $instance_current; } // Translate field title if set and it is the default one. if (!empty($instance_current['label']) && $instance_current['label'] != $instance['label']) { if (!empty($alter_element['#title']) && $alter_element['#title'] == check_plain($instance['label'])) { $alter_element['#title'] = check_plain($instance_current['label']); } } // Translate field description if set and it is the default one. if (!empty($instance_current['description']) && $instance_current['description'] != $instance['description']) { if (!empty($alter_element['#description'])) { // Allow single-value file fields and image fields to have their // descriptions translated. file_field_widget_form() passes the // description through theme('file_upload_help'), so i18n_field // must do the same. $filefield = in_array($field['type'], array('file', 'image')); $single_value = ($field['cardinality'] == 1); $no_default = empty($alter_element['#default_value']['fid']); if ($filefield && $single_value && $no_default) { $help_variables = array( 'description' => field_filter_xss($instance['description']), 'upload_validators' => isset($alter_element['#upload_validators']) ? $alter_element['#upload_validators'] : array(), ); $original_description = theme('file_upload_help', $help_variables); if ($alter_element['#description'] == $original_description) { $help_variables = array( 'description' => field_filter_xss($instance_current['description']), 'upload_validators' => isset($alter_element['#upload_validators']) ? $alter_element['#upload_validators'] : array(), ); $alter_element['#description'] = theme('file_upload_help', $help_variables); } } elseif ($alter_element['#description'] == field_filter_xss($instance['description'])) { $alter_element['#description'] = field_filter_xss($instance_current['description']); } } } // Translate list options. $has_options = (!empty($alter_element['#options']) || $field['type'] == 'list_boolean'); $has_allowed_values = !empty($field['settings']['allowed_values']); $translate = i18n_field_type_info($field['type'], 'translate_options'); if ($has_options && $has_allowed_values && $translate) { $alter_element['#options'] = $translate($field, $i18n_langcode); if (isset($alter_element['#properties']) && !empty($alter_element['#properties']['empty_option'])) { $label = theme('options_none', array('instance' => $instance, 'option' => $alter_element['#properties']['empty_option'])); $alter_element['#options'] = array('_none' => $label) + $alter_element['#options']; } // Translate list_boolean fields using the checkboxes widget. if (!empty($alter_element['#title']) && $field['type'] == 'list_boolean' && !empty($alter_element['#on_value'])) { $on_value = $alter_element['#on_value']; $alter_element['#options']; $alter_element['#title'] = $alter_element['#options'][$on_value]; // For using label instead of "On value". if ($instance['widget']['settings']['display_label']) { $alter_element['#title'] = $instance_current['label']; } } } // Check for more parameters, skip this part if missing. if (!isset($context['delta']) || !isset($context['items'])) { return; } $delta = $context['delta']; $items = $context['items']; // Translate default value. $has_default_value = (isset($alter_element['#default_value']) && !empty($instance['default_value'][$delta]['value'])); $storage_has_value = !empty($items[$delta]['value']); $translate = i18n_field_type_info($field['type'], 'translate_default'); if ($has_default_value && $storage_has_value && $translate) { // Compare the default value with the value currently in storage. if ($instance['default_value'][$delta]['value'] === $items[$delta]['value']) { $alter_element['#default_value'] = $translate($instance, $items[$delta]['value'], $i18n_langcode); } } } /** * Implements hook_field_attach_view_alter(). */ function i18n_field_field_attach_view_alter(&$output, $context) { foreach (element_children($output) as $field_name) { $element = &$output[$field_name]; if (!empty($element['#entity_type']) && !empty($element['#field_name']) && !empty($element['#bundle'])) { $instance = field_info_instance($element['#entity_type'], $element['#field_name'], $element['#bundle']); // Translate field title if set if (!empty($instance['label'])) { $element['#title'] = i18n_field_translate_property($instance, 'label'); } // Translate field description if set if (!empty($instance['description'])) { $element['#description'] = i18n_field_translate_property($instance, 'description'); } } } } /** * Implements hook_field_create_field(). */ function i18n_field_field_create_field($field) { i18n_field_field_update_strings($field); } /** * Implements hook_field_create_instance(). */ function i18n_field_field_create_instance($instance) { i18n_field_instance_update_strings($instance); } /** * Implements hook_field_delete_instance(). */ function i18n_field_field_delete_instance($instance) { i18n_string_object_remove('field_instance', $instance); } /** * Implements hook_field_update_instance(). */ function i18n_field_field_update_instance($instance, $prior_instance) { i18n_field_instance_update_strings($instance); } /** * Implements hook_field_update_field(). */ function i18n_field_field_update_field($field) { i18n_field_field_update_strings($field); } /** * Update field strings */ function i18n_field_field_update_strings($field) { i18n_string_object_update('field', $field); } /** * Update field instance strings */ function i18n_field_instance_update_strings($instance) { i18n_string_object_update('field_instance', $instance); } /** * Returns the array of translated allowed values for a list field. * * The strings are not safe for output. Keys and values of the array should be * sanitized through field_filter_xss() before being displayed. * * @param $field * The field definition. * * @return * The array of allowed values. Keys of the array are the raw stored values * (number or text), values of the array are the display labels. */ function i18n_field_translate_allowed_values($field, $langcode = NULL) { if (!empty($field['settings']['allowed_values'])) { return i18n_string_translate(array('field', $field['field_name'], '#allowed_values'), $field['settings']['allowed_values'], array('langcode' => $langcode, 'sanitize' => FALSE)); } else { return array(); } } /** * Translate field default. */ function i18n_field_translate_default($instance, $value, $langcode = NULL) { // The default value does not need sanitizing in a text_textfield widget. $sanitize = !($instance['widget']['type'] == 'text_textfield' && $instance['widget']['module'] == 'text'); return i18n_string_translate(array('field', $instance['field_name'], $instance['bundle'], 'default_value'), $value, array('langcode' => $langcode, 'sanitize' => $sanitize)); } /** * Translate field property */ function i18n_field_translate_property($instance, $property, $langcode = NULL) { // For performance reasons, we translate the whole instance once, which is cached. $instance = i18n_string_object_translate('field_instance', $instance, array('langcode' => $langcode)); return $instance[$property]; } /** * Get i18n information for translating fields. * * @param $type * Optional field type. * @param $property * Optional property to get from field type. * * @return * - The property for the field if $type and $property set. * - Array of properties for the field type if only $type is set. * - Array of translation information for all field types. */ function i18n_field_type_info($type = NULL, $property = NULL) { $info = &drupal_static(__FUNCTION__); if (!isset($info)) { $info = module_invoke_all('i18n_field_info'); drupal_alter('i18n_field_info', $info); } if ($property) { return isset($info[$type]) && isset($info[$type][$property]) ? $info[$type][$property] : NULL; } elseif ($type) { return isset($info[$type]) ? $info[$type] : array(); } else { return $info; } } /** * Implements hook_field_info_alter(). */ function i18n_field_field_info_alter(&$field_info) { foreach(array_keys($field_info) as $type) { $field_info[$type]['property_callbacks'][] = 'i18n_field_entity_property_callback'; } } /** * Prime the cache to avoid single db queries for entity fields / properties. * * This is mainly uses when large operations are occuring like a flush of the * entity_property_infos(). */ function i18n_field_prime_caches() { global $language; static $cache_primed; // Fill the cache. This should avoid single db queries when filling the // properties. if (empty($cache_primed)) { $cache_primed = TRUE; $text_group = i18n_string_textgroup('field'); // Load all strings at once to avoid callbacks for each individual string. $text_group->load_strings(); $text_group->multiple_translation_search(array('type' => '*', 'objectid' => '*', 'property' => '*'), $language->language); } } /** * Callback to translate entity property info for a fields. * * @see entity_metadata_field_entity_property_info() * @see entity_metadata_field_default_property_callback() * @see i18n_field_i18n_object_info_alter() * @see hook_module_implements_alter() */ function i18n_field_entity_property_callback(&$info, $entity_type, $field, $instance, $field_type) { global $language; // This could create a endless recursion if it's called during rebuilding the // cache for i18n_object_info(). So if the cache of i18n_object_info isn't // available yet we assume the worst case, leave the info alone but trigger a // rebuild of the property when hook_i18n_object_info_alter is invoked. At // that point the info is available and we can rely on it. if (!$info = &drupal_static('i18n_object_info')) { $i18n_field_entity_property_callback_fallback = &drupal_static(__FUNCTION__); $i18n_field_entity_property_callback_fallback = TRUE; return; } i18n_field_prime_caches(); $name = $field['field_name']; $property = &$info[$entity_type]['bundles'][$instance['bundle']]['properties'][$name]; $property['label'] = i18n_field_translate_property($instance, 'label', $language->language); } /** * Implements hook_i18n_object_info_alter(). */ function i18n_field_i18n_object_info_alter(&$info) { if (drupal_static('i18n_field_entity_property_callback')) { if ($info = drupal_static('i18n_object_info')) { // Clean static and permanent cache of the data and then re-run the property // building. // Use a lock to avoid stampeding. $lock_name = 'i18n_field_entity_property_callback_fallback:' . $GLOBALS['language']->language; // See if another request is already doing this. If so we bail out here as // we won't help with anything at the moment. if (!lock_may_be_available($lock_name)) { return; } if (lock_acquire($lock_name)) { i18n_field_prime_caches(); // Inject translated properties. $entity_property_info = entity_get_property_info(); foreach ($entity_property_info as $entity_type => $properties) { if (isset($properties['bundles'])) { foreach ($properties['bundles'] as $bundle => $bundle_properties) { if ($bundle_properties['properties']) { foreach ($bundle_properties['properties'] as $bundle_property => $bundle_property_info) { if ($instance = field_info_instance($entity_type, $bundle_property, $bundle)) { $property = &$entity_property_info[$entity_type]['bundles'][$instance['bundle']]['properties'][$bundle_property]; $property['label'] = i18n_field_translate_property($instance, 'label', $GLOBALS['language']->language); } } } } } } // Inject into static cache. $entity_get_property_info = &drupal_static('entity_get_property_info', array()); $entity_get_property_info = $entity_property_info; // Write permanent cache. cache_set('entity_property_info:' . $GLOBALS['language']->language, $entity_property_info); lock_release($lock_name); } } else { watchdog('i18n_field', 'Unable to run fall-back handling for entity property translation due missing "i18n_object_info" cache', array(), WATCHDOG_WARNING); } } } /** * Implements hook_module_implements_alter(). */ function i18n_field_module_implements_alter(&$implementations, $hook) { if ($hook == 'i18n_object_info_alter') { // Move our hook implementation to the bottom. $group = $implementations['i18n_field']; unset($implementations['i18n_field']); $implementations['i18n_field'] = $group; } }