diff --git a/entity_translation.module b/entity_translation.module index 6ef4200..4183852 100644 --- a/entity_translation.module +++ b/entity_translation.module @@ -244,6 +244,13 @@ function entity_translation_menu() { 'file' => 'entity_translation.admin.inc', ); + $items['entity_translation/taxonomy_term/autocomplete'] = array( + 'title' => 'Entity translation autocomplete', + 'page callback' => 'entity_translation_taxonomy_term_autocomplete', + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); + return $items; } @@ -1597,6 +1604,9 @@ function entity_translation_field_attach_submit($entity_type, $entity, $form, &$ // Update the wrapped entity with the submitted values. $handler->setEntity($entity); $handler->entityFormSubmit($form, $form_state); + + // Process in-place translations for the taxonomy autocomplete widget. + entity_translation_taxonomy_term_field_attach_submit($entity_type, $entity, $form, $form_state); } } @@ -1671,7 +1681,7 @@ function theme_entity_translation_language_tabs($variables) { * Adds an option to enable field synchronization. * Enable a selector to choose whether a field is translatable. */ -function entity_translation_form_field_ui_field_edit_form_alter(&$form, $form_state) { +function entity_translation_form_field_ui_field_edit_form_alter(&$form, &$form_state) { $instance = $form['#instance']; $entity_type = $instance['entity_type']; $field_name = $instance['field_name']; @@ -1691,7 +1701,8 @@ function entity_translation_form_field_ui_field_edit_form_alter(&$form, $form_st $label = t('Field translation'); $title = t('Users may translate all occurrences of this field:') . _entity_translation_field_desc($field); - if (field_has_data($field)) { + $form_state['field_has_data'] = field_has_data($field); + if ($form_state['field_has_data']) { $path = "admin/config/regional/entity_translation/translatable/$field_name"; $status = $translatable ? $title : (t('All occurrences of this field are untranslatable:') . _entity_translation_field_desc($field)); $link_title = !$translatable ? t('Enable translation') : t('Disable translation'); @@ -1719,6 +1730,11 @@ function entity_translation_form_field_ui_field_edit_form_alter(&$form, $form_st '#default_value' => $translatable, ); } + + $function = 'entity_translation_form_field_ui_field_edit_' . $instance['widget']['type'] . '_form_alter'; + if (function_exists($function)) { + $function($form, $form_state); + } } /** diff --git a/entity_translation.taxonomy.inc b/entity_translation.taxonomy.inc index b47a12f..ab85fea 100644 --- a/entity_translation.taxonomy.inc +++ b/entity_translation.taxonomy.inc @@ -97,3 +97,462 @@ function entity_translation_form_taxonomy_form_vocabulary_submit($form, &$form_s variable_set('entity_translation_taxonomy', $info); } +/** + * Returns a translated label for the specified taxonomy term. + * + * @param object $term + * A taxonomy term object. + * @param string $langcode + * The language the label should be translated in. + * + * @return string + * The translated taxonomy term label. + * + * @internal This is supposed to be used only by the ET taxonomy integration + * code, as it might be removed/replaced in any moment of the ET lifecycle. + */ +function _entity_translation_taxonomy_label($term, $langcode) { + $entity_type = 'taxonomy_term'; + if (function_exists('title_entity_label')) { + $label = title_entity_label($term, $entity_type, $langcode); + } + else { + $label = entity_label($entity_type, $term); + } + return (string) $label; +} + +/** + * Implements entity_translation_form_field_ui_field_edit_WIDGET_TYPE_form_alter(). + * + * {@inheritdoc} + */ +function entity_translation_form_field_ui_field_edit_taxonomy_autocomplete_form_alter(&$form, &$form_state) { + $key = 'entity_translation_taxonomy_autocomplete_translate'; + $instance = $form['#instance']; + $field_name = $instance['field_name']; + $entity_type = $instance['entity_type']; + $field = field_info_field($field_name); + $translatable = field_is_translatable($entity_type, $field); + + // Add a checkbox to toggle in-place translation for autocomplete widgets. + $form['instance']['settings'][$key] = array( + '#type' => 'checkbox', + '#title' => t('Enable in-place translation of terms'), + '#description' => t('Check this option if you wish to use translation forms to perform in-place translation for terms entered in the original language.'), + '#default_value' => !$translatable && !empty($form['#instance']['settings'][$key]), + '#access' => !$form_state['field_has_data'] || !$translatable, + '#states' => array( + 'visible' => array(':input[name="field[translatable]"]' => array('checked' => FALSE)), + ), + '#weight' => -8, + ); +} + +/** + * Checks whether in-place translation is enabled for the autocomplete widget. + * + * @param array $element + * The widget form element. + * @param array $form_state + * The form state array. + * + * @return bool + * TRUE if in-place translation is enabled, FALSE otherwise. + */ +function _entity_translation_taxonomy_autocomplete_translation_enabled($element, $form_state) { + $field = field_info_field($element['#field_name']); + if (field_is_translatable($element['#entity_type'], $field)) { + return FALSE; + } + + $entity_type = 'taxonomy_term'; + $parent_handler = entity_translation_get_handler($element['#entity_type'], $element['#entity']); + $active_langcode = $parent_handler->getActiveLanguage(); + $translations = $parent_handler->getTranslations(); + $entity_langcode = isset($translations->original) ? $translations->original : LANGUAGE_NONE; + list(,, $bundle) = entity_extract_ids($element['#entity_type'], $element['#entity']); + $instance = field_info_instance($element['#entity_type'], $field['field_name'], $bundle); + + // We need to make sure that we are not dealing with a translation form. + // However checking the active language is not enough, because the user may + // have changed the entity language. + return + (isset($form_state['entity_translation']['is_translation']) ? + $form_state['entity_translation']['is_translation'] : ($active_langcode != $entity_langcode)) && + !empty($instance['settings']['entity_translation_taxonomy_autocomplete_translate']) && + (user_access('translate any entity') || user_access("translate $entity_type entities")); +} + +/** + * Implements hook_field_widget_WIDGET_TYPE_form_alter(). + * + * {@inheritdoc} + */ +function entity_translation_field_widget_taxonomy_autocomplete_form_alter(&$element, &$form_state, $context) { + // The autocomplete widget is also displayed in the field configuration form, + // in which case we do not need to perform any alteration. + if (!isset($element['#entity'])) { + return; + } + + // We will need to translate term names, if Title is enabled and configured + // for this vocabulary. + $entity_type = 'taxonomy_term'; + $field = field_widget_field($element, $form_state); + $bundle = !empty($field['settings']['allowed_values'][0]['vocabulary']) ? $field['settings']['allowed_values'][0]['vocabulary'] : NULL; + if ($bundle && entity_translation_enabled($entity_type, $bundle)) { + $parent_handler = entity_translation_get_handler($element['#entity_type'], $element['#entity']); + $langcode = $parent_handler->getActiveLanguage(); + $terms = array_values(_entity_translation_taxonomy_autocomplete_widget_get_terms($element)); + + // If we are using the regular autocomplete behavior also in translation + // forms, we need to set our custom callback. + if (!_entity_translation_taxonomy_autocomplete_translation_enabled($element, $form_state)) { + $element['#autocomplete_path'] = 'entity_translation/' . $entity_type . '/autocomplete/' . $langcode . '/' . $element['#field_name']; + $translations = $parent_handler->getTranslations(); + if (isset($translations->original) && $translations->original != $langcode) { + $labels = array(); + foreach ($terms as $delta => $term) { + $labels[] = _entity_translation_taxonomy_label($term, $langcode); + } + $element['#default_value'] = implode(', ', $labels); + } + } + // Otherwise we just provide teh in-place translation widget. + else { + $element['#type'] = 'fieldset'; + $element['#description'] = t('Enter one translation for each term'); + $element['#access'] = (bool) $terms; + foreach ($terms as $delta => $term) { + $element[$delta] = array( + '#type' => 'textfield', + '#default_value' => _entity_translation_taxonomy_label($term, $langcode), + '#required' => TRUE, + '#tid' => $term->tid, + ); + } + $element['#process'][] = 'entity_translation_taxonomy_autocomplete_process'; + } + + // The native term save logic is performed at widget validation level, so we + // just replace the validation handler to provide our logic instead. + $element['#element_validate'] = array_values(array_diff($element['#element_validate'], array('taxonomy_autocomplete_validate'))); + $element['#element_validate'][] = 'entity_translation_taxonomy_autocomplete_validate'; + } +} + +/** + * Returns the terms referenced by the taxonomy autocomplete widget field. + * + * @param array $element + * The taxonomy autocomplete form element. + * + * @return object[] + * An associative array of taxonomy term object keyed by their identifiers. + */ +function _entity_translation_taxonomy_autocomplete_widget_get_terms($element) { + $items = isset($element['#entity']->{$element['#field_name']}[$element['#language']]) ? + $element['#entity']->{$element['#field_name']}[$element['#language']] : array(); + $tids = array_map(function ($item) { return $item['tid']; }, $items); + return taxonomy_term_load_multiple($tids); +} + +/** + * Process callback for the ET taxonomy autocomplete widget. + * + * {@inheritdoc} + */ +function entity_translation_taxonomy_autocomplete_process($element) { + // The in-place translation widget makes sense only for untranslatable field, + // which may have the "(all languages)" label suffix. In this case it would be + // confusing so we need to revert that. + $instance = field_info_instance($element['#entity_type'], $element['#field_name'], $element['#bundle']); + $element['#title'] = check_plain($instance['label']); + return $element; +} + +/** + * Entity translation taxonomy autocomplete callback. + * + * @param string $langcode + * The input language. + * @param string $field_name + * The name of the term reference field. + * @param string $tags_typed + * (optional) A comma-separated list of term names entered in the + * autocomplete form element. Only the last term is used for autocompletion. + * Defaults to an empty string. + * + * @see taxonomy_autocomplete() + */ +function entity_translation_taxonomy_term_autocomplete($langcode = NULL, $field_name = '', $tags_typed = '') { + // If the request has a '/' in the search text, then the menu system will have + // split it into multiple arguments, recover the intended $tags_typed. + $args = func_get_args(); + // Shift off the $langcode and $field_name arguments. + array_shift($args); + array_shift($args); + $tags_typed = implode('/', $args); + + // Make sure the field exists and is a taxonomy field. + if (!($field = field_info_field($field_name)) || $field['type'] !== 'taxonomy_term_reference') { + // Error string. The JavaScript handler will realize this is not JSON and + // will display it as debugging information. + print t('Taxonomy field @field_name not found.', array('@field_name' => $field_name)); + exit; + } + + // The user enters a comma-separated list of tags. We only autocomplete the + // last tag. + $tags_typed = drupal_explode_tags($tags_typed); + $tag_last = drupal_strtolower(array_pop($tags_typed)); + + $term_matches = array(); + if ($tag_last != '') { + if (!isset($langcode) || $langcode == LANGUAGE_NONE) { + $langcode = $GLOBALS['language_content']->language; + } + + // Part of the criteria for the query come from the field's own settings. + $vocabulary = _entity_translation_taxonomy_reference_get_vocabulary($field); + + $entity_type = 'taxonomy_term'; + $query = new EntityFieldQuery(); + $query->addTag('taxonomy_term_access'); + $query->entityCondition('entity_type', $entity_type); + + // If the Title module is enabled and the taxonomy term name is replaced for + // the current bundle, we can look for translated names, otherwise we fall + // back to the regular name property. + if (module_invoke('title', 'field_replacement_enabled', $entity_type, $vocabulary->machine_name, 'name')) { + $name_field = 'name_field'; + $language_group = 0; + // Do not select already entered terms. + $column = 'value'; + if (!empty($tags_typed)) { + $query->fieldCondition($name_field, $column, $tags_typed, 'NOT IN', NULL, $language_group); + } + $query->fieldCondition($name_field, $column, $tag_last, 'CONTAINS', NULL, $language_group); + $query->fieldLanguageCondition($name_field, array($langcode, LANGUAGE_NONE), NULL, NULL, $language_group); + } + else { + $name_field = 'name'; + // Do not select already entered terms. + if (!empty($tags_typed)) { + $query->propertyCondition($name_field, $tags_typed, 'NOT IN'); + } + $query->propertyCondition($name_field, $tag_last, 'CONTAINS'); + } + + // Select rows that match by term name. + $query->propertyCondition('vid', $vocabulary->vid); + $query->range(0, 10); + $result = $query->execute(); + + // Populate the results array. + $prefix = count($tags_typed) ? drupal_implode_tags($tags_typed) . ', ' : ''; + $terms = !empty($result[$entity_type]) ? taxonomy_term_load_multiple(array_keys($result[$entity_type])) : array(); + foreach ($terms as $tid => $term) { + $name = _entity_translation_taxonomy_label($term, $langcode); + $n = $name; + // Term names containing commas or quotes must be wrapped in quotes. + if (strpos($name, ',') !== FALSE || strpos($name, '"') !== FALSE) { + $n = '"' . str_replace('"', '""', $name) . '"'; + } + $term_matches[$prefix . $n] = check_plain($name); + } + } + + drupal_json_output($term_matches); +} + +/** + * Returns the vocabulary enabled for the specified taxonomy reference field. + * + * @param array $field + * A field definition. + * + * @return object|null + * A vocabulary object or NULL if none is found. + */ +function _entity_translation_taxonomy_reference_get_vocabulary($field) { + $vocabulary = NULL; + if (!empty($field['settings']['allowed_values'])) { + $vids = array(); + $vocabularies = taxonomy_vocabulary_get_names(); + foreach ($field['settings']['allowed_values'] as $tree) { + $vids[] = $vocabularies[$tree['vocabulary']]->vid; + } + $vocabulary = taxonomy_vocabulary_load(reset($vids)); + } + return $vocabulary; +} + +/** + * Form element validate handler for taxonomy term autocomplete element. + * + * {@inheritdoc} + * + * @see taxonomy_autocomplete_validate() + */ +function entity_translation_taxonomy_autocomplete_validate($element, &$form_state) { + $value = array(); + list($id) = entity_extract_ids($element['#entity_type'], $element['#entity']); + $is_new = !isset($id); + + // This is the language of the parent entity, that we will be applying to new + // terms. + $parent_handler = entity_translation_get_handler($element['#entity_type'], $element['#entity']); + $langcode = !empty($form_state['entity_translation']['form_langcode']) ? + $form_state['entity_translation']['form_langcode'] : $parent_handler->getActiveLanguage(); + + // Handle in-place translation. + if (_entity_translation_taxonomy_autocomplete_translation_enabled($element, $form_state)) { + // The referenced terms cannot change, so we just need to collect their term + // identifiers. We also build a map of the corresponding deltas for later + // use. + $deltas = array(); + foreach (element_children($element) as $delta) { + $tid = $element[$delta]['#tid']; + $deltas[$tid] = $delta; + $value[$delta]['tid'] = $tid; + } + + // Save term translations. + $entity_type = 'taxonomy_term'; + $name_field = 'name_field'; + $source_langcode = $parent_handler->getSourceLanguage(); + // This is a validation handler, so we must defer the actual save to the + // submit phase. + $terms_to_save = &$form_state['entity_translation']['taxonomy_autocomplete'][$element['#entity_type']][$id][$element['#field_name']]; + foreach (taxonomy_term_load_multiple(array_keys($deltas)) as $term) { + // This is also the right context to perform validation. + $term_translation = $element[$deltas[$term->tid]]['#value']; + if (!$term_translation) { + $instance = field_info_instance($element['#entity_type'], $element['#field_name'], $element['#bundle']); + drupal_set_message(t('The translations for %field_label cannot be empty.', array('%field_label' => $instance['label'])), 'error', FALSE); + continue; + } + + $handler = entity_translation_get_handler($entity_type, $term); + $translations = $handler->getTranslations(); + $term_langcode = $handler->getLanguage(); + $typed_langcode = $term_langcode != LANGUAGE_NONE ? $langcode : $term_langcode; + + // Create a new translation in the active language, if it is missing. + if (!isset($translations->data[$typed_langcode]) && $typed_langcode != LANGUAGE_NONE) { + $translation = array( + 'language' => $typed_langcode, + 'source' => $source_langcode, + 'uid' => $GLOBALS['user']->uid, + 'status' => 1, + 'created' => REQUEST_TIME, + 'changed' => REQUEST_TIME, + ); + $translation_values = array( + $name_field => array($typed_langcode => array(array('value' => $term_translation))), + ); + $handler->setTranslation($translation, $translation_values); + $terms_to_save[] = $term; + } + // Otherwise we just update the existing translation, if it has changed. + // If the term is language-neutral, we just update its main value. This is + // expected to happen normally, but could when referencing existing terms. + elseif ($term_translation != _entity_translation_taxonomy_label($term, $typed_langcode)) { + $term->{$name_field}[$typed_langcode][0]['value'] = $term_translation; + $terms_to_save[] = $term; + } + } + } + // Autocomplete widgets do not send their tids in the form, so we must detect + // them here and process them independently. + elseif ($tags = $element['#value']) { + $entity_type = 'taxonomy_term'; + $field = field_widget_field($element, $form_state); + $vocabulary = _entity_translation_taxonomy_reference_get_vocabulary($field); + $typed_tags = drupal_explode_tags($tags); + + // Collect existing terms by name. + $existing_terms = array(); + foreach (_entity_translation_taxonomy_autocomplete_widget_get_terms($element) as $term) { + $name = _entity_translation_taxonomy_label($term, $langcode); + $existing_terms[$name] = $term; + } + + // Select terms that match by the (translated) name. + $query = new EntityFieldQuery(); + $query->addTag('taxonomy_term_access'); + $query->entityCondition('entity_type', $entity_type); + $query->propertyCondition('vid', $vocabulary->vid); + if ($langcode != LANGUAGE_NONE && module_invoke('title', 'field_replacement_enabled', $entity_type, $vocabulary->machine_name, 'name')) { + $language_group = 0; + // Do not select already entered terms. + $name_field = 'name_field'; + $column = 'value'; + $query->fieldCondition($name_field, $column, $typed_tags, NULL, NULL, $language_group); + // When we are creating a new entity, we cannot filter by active language, + // as that may have not be applied to the autocomplete query. + if (!$is_new) { + $query->fieldLanguageCondition($name_field, array($langcode, LANGUAGE_NONE), NULL, NULL, $language_group); + } + } + else { + $query->propertyCondition('name', $typed_tags); + } + $result = $query->execute(); + + // When we are creating a new entity, the language used for the autocomplete + // query is the current content language, so we should use that to update + // the map of existing terms. + if (!empty($result[$entity_type])) { + $typed_langcode = !$is_new ? $langcode : $GLOBALS['language_content']->language; + foreach (taxonomy_term_load_multiple(array_keys($result[$entity_type])) as $term) { + $name = _entity_translation_taxonomy_label($term, $typed_langcode); + $existing_terms[$name] = $term; + } + } + + // Now collect the identifiers for the various terms and update the taxonomy + // reference field values. + foreach ($typed_tags as $delta => $typed_tag) { + // See if the term exists in the chosen vocabulary and return the tid. + // Otherwise create a new 'autocreate' term for insert/update. + if (isset($existing_terms[$typed_tag])) { + $term = $existing_terms[$typed_tag]; + } + else { + $term = (object) array( + 'tid' => 'autocreate', + 'vid' => $vocabulary->vid, + 'name' => $typed_tag, + 'vocabulary_machine_name' => $vocabulary->machine_name, + ); + $handler = entity_translation_get_handler($entity_type, $term); + $handler->setOriginalLanguage($langcode); + $handler->initTranslations(); + } + $value[] = (array) $term; + } + } + + form_set_value($element, $value, $form_state); +} + +/** + * Term-specific implementation of hook_field_attach_submit(). + */ +function entity_translation_taxonomy_term_field_attach_submit($entity_type, $entity, $form, &$form_state) { + // Finally save in-place translations + if (!empty($form_state['entity_translation']['taxonomy_autocomplete'])) { + foreach ($form_state['entity_translation']['taxonomy_autocomplete'] as $entity_type => $entity_type_data) { + foreach ($entity_type_data as $id => $field_name_data) { + foreach ($field_name_data as $field_name => $term_data) { + foreach ($term_data as $term) { + entity_translation_entity_save('taxonomy_term', $term); + } + } + } + } + } +}