diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 277c717..db420ee 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -10,7 +10,6 @@ Entity translation 7.x-1.x, xxxx-xx-xx switch operation. #1279372 by getgood, loganfsmyth, evolvingweb, plach, Kristen Pol: Enable bulk field language updates when switching field translatability. -#1155134 by das-peter, GiorgosK, zambrey: Added Integrate pathauto. #1380380 by bojanz: Fixed Prevent notice when translation has been removed from the form. #1367832 by floretan: Fixed Check #parents and #field_parents() for source diff --git a/entity-translation.css b/entity-translation.css new file mode 100644 index 0000000..33b3abc --- /dev/null +++ b/entity-translation.css @@ -0,0 +1,4 @@ +.entity-translation-language-tabs { + clear: both; + padding-top: 0.75em; +} diff --git a/entity_translation.admin.inc b/entity_translation.admin.inc index 05a8e96..5a99e66 100644 --- a/entity_translation.admin.inc +++ b/entity_translation.admin.inc @@ -6,7 +6,7 @@ */ /** - * The entity translation settings form. + * Builder function for the entity translation settings form. */ function entity_translation_admin_form($form, $form_state) { $options = array(); @@ -18,6 +18,13 @@ function entity_translation_admin_form($form, $form_state) { '#default_value' => variable_get('locale_field_language_fallback', TRUE), ); + $form['entity_translation_shared_labels'] = array( + '#type' => 'checkbox', + '#title' => t('Display shared labels'), + '#description' => t('Append a "Shared field" hint to entity form widgets shared accross translations.'), + '#default_value' => variable_get('entity_translation_shared_labels', TRUE), + ); + foreach (entity_get_info() as $entity_type => $info) { if ($info['fieldable']) { $options[$entity_type] = $info['label']; @@ -50,7 +57,7 @@ function entity_translation_admin_form_submit($form, $form_state) { } /** - * Translations overview menu callback. + * Translations overview page callback. */ function entity_translation_overview($entity_type, $entity, $callback = NULL) { // Entity translation and node translation share the same system path. @@ -76,76 +83,86 @@ function entity_translation_overview($entity_type, $entity, $callback = NULL) { } $header = array(t('Language'), t('Source language'), t('Translation'), t('Status'), t('Operations')); - // @todo: Do we want only enabled languages here? - $languages = language_list(); - $source = isset($_SESSION['entity_translation_source_language']) ? $_SESSION['entity_translation_source_language'] : $translations->original; + $languages = entity_translation_languages(); + $source = $translations->original; $base_path = $handler->getBasePath(); $path = $handler->getViewPath(); + $rows = array(); - if ($path) { + if (drupal_multilingual()) { // If we have a view path defined for the current entity get the switch // links based on it. - $links = language_negotiation_get_switch_links(LANGUAGE_TYPE_CONTENT, $path); - } + if ($path) { + $links = EntityTranslationDefaultHandler::languageSwitchLinks($path); + } + + foreach ($languages as $language) { + $options = array(); + $language_name = $language->name; + $langcode = $language->language; + $edit_path = $handler->getEditPath($langcode); + $add_path = "$base_path/edit/add/$source/$langcode"; - foreach ($languages as $language) { - $options = array(); - $language_name = $language->name; - $langcode = $language->language; + if ($base_path) { + $add_links = EntityTranslationDefaultHandler::languageSwitchLinks($add_path); + $edit_links = EntityTranslationDefaultHandler::languageSwitchLinks($edit_path); + } - if (isset($translations->data[$langcode])) { - list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity); + if (isset($translations->data[$langcode])) { + list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity); - // Existing translation in the translation set: display status. - $is_original = $langcode == $translations->original; - $translation = $translations->data[$langcode]; - $label = _entity_translation_label($entity_type, $entity, $langcode); - $link = isset($links->links[$langcode]['href']) ? $links->links[$langcode] : array('href' => $path, 'language' => $language); - $row_title = l($label, $link['href'], $link); + // Existing translation in the translation set: display status. + $is_original = $langcode == $translations->original; + $translation = $translations->data[$langcode]; + $label = _entity_translation_label($entity_type, $entity, $langcode); + $link = isset($links->links[$langcode]['href']) ? $links->links[$langcode] : array('href' => $path, 'language' => $language); + $row_title = l($label, $link['href'], $link); - if (empty($link['href'])) { - $row_title = $is_original ? $label : t('n/a'); - } + if (empty($link['href'])) { + $row_title = $is_original ? $label : t('n/a'); + } - $edit_path = $is_original ? $handler->getEditPath() : $base_path . '/translate/edit/' . $langcode; - if ($edit_path && $handler->getAccess('update')) { - $options[] = l($is_original ? t('edit') : t('edit translation'), $edit_path); - } + if ($edit_path && $handler->getAccess('update') && $handler->getTranslationAccess($langcode)) { + $link = isset($edit_links->links[$langcode]['href']) ? $edit_links->links[$langcode] : array('href' => $edit_path, 'language' => $language); + $options[] = l(t('edit'), $link['href'], $link); + } - $status = $translation['status'] ? t('Published') : t('Not published'); - $status .= isset($translation['translate']) && $translation['translate'] ? ' - ' . t('outdated') . '' : ''; + $status = $translation['status'] ? t('Published') : t('Not published'); + // @todo Add a theming function here. + $status .= isset($translation['translate']) && $translation['translate'] ? ' - ' . t('outdated') . '' : ''; - if ($is_original) { - $language_name = t('@language_name', array('@language_name' => $language_name)); - $source_name = t('(original content)'); + if ($is_original) { + $language_name = t('@language_name', array('@language_name' => $language_name)); + $source_name = t('(original content)'); + } + else { + $source_name = $languages[$translation['source']]->name; + } } else { - $source_name = $languages[$translation['source']]->name; - } - } - else { - // No such translation in the set yet: help user to create it. - $row_title = $source_name = t('n/a'); - $add_path = "$base_path/translate/add/$langcode/$source"; - - if ($source != $langcode && $handler->getAccess('update')) { - list(, , $bundle) = entity_extract_ids($entity_type, $entity); - $translatable = FALSE; - - foreach (field_info_instances($entity_type, $bundle) as $instance) { - $field_name = $instance['field_name']; - $field = field_info_field($field_name); - if ($field['translatable']) { - $translatable = TRUE; - break; + // No such translation in the set yet: help user to create it. + $row_title = $source_name = t('n/a'); + + if ($source != $langcode && $handler->getAccess('update')) { + list(, , $bundle) = entity_extract_ids($entity_type, $entity); + $translatable = FALSE; + + foreach (field_info_instances($entity_type, $bundle) as $instance) { + $field_name = $instance['field_name']; + $field = field_info_field($field_name); + if ($field['translatable']) { + $translatable = TRUE; + break; + } } - } - $options[] = $translatable ? l(t('add translation'), $add_path) : t('No translatable fields'); + $link = isset($add_links->links[$langcode]['href']) ? $add_links->links[$langcode] : array('href' => $add_path, 'language' => $language); + $options[] = $translatable ? l(t('add'), $link['href'], $link) : t('No translatable fields'); + } + $status = t('Not translated'); } - $status = t('Not translated'); + $rows[] = array($language_name, $source_name, $row_title, $status, implode(" | ", $options)); } - $rows[] = array($language_name, $source_name, $row_title, $status, implode(" | ", $options)); } drupal_set_title(t('Translations of %label', array('%label' => $handler->getLabel())), PASS_THROUGH); @@ -164,7 +181,7 @@ function entity_translation_overview($entity_type, $entity, $callback = NULL) { } /** - * Call the appropriate translation overview callback. + * Calls the appropriate translation overview callback. */ function entity_translation_overview_callback($callback, $entity) { if (module_exists($callback['module'])) { @@ -177,7 +194,7 @@ function entity_translation_overview_callback($callback, $entity) { } /** - * Return the appropriate entity label for the given language. + * Returns the appropriate entity label for the given language. */ function _entity_translation_label($entity_type, $entity, $langcode = NULL) { if (function_exists('title_entity_label')) { @@ -192,300 +209,6 @@ function _entity_translation_label($entity_type, $entity, $langcode = NULL) { } /** - * Translation adding/editing form. - */ -function entity_translation_edit_form($form, $form_state, $entity_type, $entity, $langcode, $source = NULL) { - if (entity_translation_node($entity_type, $entity)) { - drupal_goto("node/$entity->nid/translate"); - } - - $handler = entity_translation_get_handler($entity_type, $entity); - - $languages = language_list(); - $args = array('@label' => $handler->getLabel(), '@language' => t($languages[$langcode]->name)); - drupal_set_title(t('@label [@language translation]', $args)); - - $translations = $handler->getTranslations(); - $new_translation = !isset($translations->data[$langcode]); - - $form = array( - '#handler' => $handler, - '#entity_type' => $entity_type, - '#entity' => $entity, - '#source' => $new_translation ? $source : $translations->data[$langcode]['source'], - '#language' => $langcode, - ); - - // Display source language selector only if we are creating a new translation - // and there are at least two translations available. - if ($new_translation && count($translations->data) > 1) { - $form['source_language'] = array( - '#type' => 'fieldset', - '#title' => t('Source language'), - '#collapsible' => TRUE, - '#collapsed' => TRUE, - '#tree' => TRUE, - '#weight' => -22, - 'language' => array( - '#type' => 'select', - '#default_value' => $source, - '#options' => array(), - ), - 'submit' => array( - '#type' => 'submit', - '#value' => t('Change'), - '#submit' => array('entity_translation_edit_form_source_language_submit'), - ), - ); - foreach (language_list() as $language) { - if (isset($translations->data[$language->language])) { - $form['source_language']['language']['#options'][$language->language] = t($language->name); - } - } - } - - $translate = intval(isset($translations->data[$langcode]) && $translations->data[$langcode]['translate']); - - $form['translation'] = array( - '#type' => 'fieldset', - '#title' => t('Translation settings'), - '#collapsible' => TRUE, - '#collapsed' => !$translate, - '#tree' => TRUE, - '#weight' => -24, - ); - $form['translation']['status'] = array( - '#type' => 'checkbox', - '#title' => t('This translation is published'), - '#default_value' => !isset($translations->data[$langcode]) || $translations->data[$langcode]['status'], - '#description' => t('When this option is unchecked, this translation will not be visible for non-administrators.'), - ); - $form['translation']['translate'] = array( - '#type' => 'checkbox', - '#title' => t('This translation needs to be updated'), - '#default_value' => $translate, - '#description' => t('When this option is checked, this translation needs to be updated because the source post has changed. Uncheck when the translation is up to date again.'), - '#disabled' => !$translate, - ); - - // If we are creating a new translation we need to retrieve form elements - // populated with the source language values, but only if form is not being - // rebuilt. In this case source values have already been populated, so we need - // to preserve possible changes. - $prepare_fields = $new_translation && !$form_state['rebuild']; - if ($prepare_fields) { - $source_form = array(); - $source_form_state = $form_state; - field_attach_form($entity_type, $entity, $source_form, $source_form_state, $source); - } - field_attach_form($entity_type, $entity, $form, $form_state, $langcode); - - list(, , $bundle) = entity_extract_ids($entity_type, $entity); - foreach (field_info_instances($entity_type, $bundle) as $instance) { - $field_name = $instance['field_name']; - $field = field_info_field($field_name); - // If a field is not translatable remove it from the translation form. - if (!$field['translatable']) { - $form[$field_name]['#access'] = FALSE; - } - // If we are creating a new translation we have to change the form item - // language information from source to target language, this way the - // user can find the form items already populated with the source values - // while the field form element holds the correct language information. - elseif ($prepare_fields && !isset($entity->{$field_name}[$langcode]) && isset($source_form[$field_name][$source])) { - $form[$field_name][$langcode] = $source_form[$field_name][$source]; - // Update #language keys in the field form subtree. - _entity_translation_form_language($form[$field_name][$langcode], $source, $langcode); - } - } - - $form['actions'] = array('#type' => 'actions'); - $form['actions']['submit'] = array( - '#type' => 'submit', - '#value' => t('Save translation'), - '#validate' => array('entity_translation_edit_form_save_validate'), - '#submit' => array('entity_translation_edit_form_save_submit'), - ); - if (!$new_translation) { - $form['actions']['delete'] = array( - '#type' => 'submit', - '#value' => t('Delete translation'), - '#submit' => array('entity_translation_edit_form_delete_submit'), - ); - } - - // URL alias widget. - if (_entity_translation_path_enabled($handler)) { - $alias = db_select('url_alias') - ->fields('url_alias', array('alias')) - ->condition('source', $handler->getViewPath()) - ->condition('language', $langcode) - ->execute() - ->fetchField(); - - $form['path'] = array( - '#type' => 'fieldset', - '#title' => t('URL path settings'), - '#tree' => TRUE, - ); - - $form['path']['alias'] = array( - '#type' => 'textfield', - '#title' => t('URL alias'), - '#default_value' => $alias, - '#maxlength' => 255, - '#collapsible' => TRUE, - '#collapsed' => TRUE, - '#description' => t('Optionally specify an alternative URL by which this entity can be accessed. For example, type "about" when writing an about page. Use a relative path and don\'t add a trailing slash or the URL alias won\'t work.'), - '#access' => user_access('create url aliases'), - '#weight' => 0, - ); - - $form['path']['source'] = array( - '#type' => 'value', - '#value' => $handler->getViewPath(), - ); - $form['path']['language'] = array( - '#type' => 'value', - '#value' => $langcode, - ); - - if (!empty($alias)) { - $pid = db_select('url_alias') - ->fields('url_alias', array('pid')) - ->condition('alias', $alias) - ->condition('language', $langcode) - ->execute() - ->fetchField(); - - $form['path']['pid'] = array( - '#type' => 'value', - '#value' => $pid, - ); - } - - // Load special form settings if applicable. - if ($entity_type == 'node') { - // Create temporary, decoupled stuff to avoid side effects. - $form_state_dummy = $form_state; - $form_state_dummy['node'] = clone($form['#entity']); - $form_state_dummy['node']->language = $langcode; - if (!isset($translations->data[$langcode])) { - // If this is a new translation simulate a new node for pathauto. - unset($form_state_dummy['node']->nid); - } - - // The following part recycles code from other modules, what means that - // this module doesn't have control about essential functional parts. - // Thus this section has to be considered as fragile as changes in the - // other modules can easily break it. - if (module_exists('pathauto')) { - // Reuse code parts from pathauto. - module_load_include('module', 'pathauto'); - pathauto_form_node_form_alter($form, $form_state_dummy); - } - if (module_exists('redirect')) { - // Reuse code parts from redirect. - module_load_include('module', 'redirect'); - redirect_form_node_form_alter($form, $form_state); - } - // Remove the temporary stuff. - unset($form_state_dummy); - } - } - - return $form; -} - -/** - * Helper function: recursively replace the source language with the given one. - */ -function _entity_translation_form_language(&$element, $source, $langcode) { - // Iterate through the form structure recursively. - foreach (element_children($element) as $key) { - _entity_translation_form_language($element[$key], $source, $langcode); - } - - // Replace specific occurrences of the source language with the target - // language. - foreach (element_properties($element) as $key) { - if ($key === '#language') { - $element[$key] = $langcode; - } - - if ($key === '#parents' || $key === '#field_parents') { - foreach ($element[$key] as $delta => $value) { - if ($value === $source) { - $element[$key][$delta] = $langcode; - } - } - } - } -} - -/** - * Submit handler for the source language selector. - */ -function entity_translation_edit_form_source_language_submit($form, &$form_state) { - $handler = $form['#handler']; - $langcode = $form_state['values']['source_language']['language']; - $path = "{$handler->getBasePath()}/translate/add/{$form['#language']}/$langcode"; - $form_state['redirect'] = array('path' => $path); - $languages = language_list(); - drupal_set_message(t('Source translation set to: %language', array('%language' => t($languages[$langcode]->name)))); -} - -/** - * Validation handler for the translation saving. - */ -function entity_translation_edit_form_save_validate($form, &$form_state) { - field_attach_form_validate($form['#entity_type'], $form['#entity'], $form, $form_state); -} - -/** - * Submit handler for the translation saving. - */ -function entity_translation_edit_form_save_submit($form, &$form_state) { - $handler = $form['#handler']; - - $translation = array( - 'translate' => $form_state['values']['translation']['translate'], - 'status' => $form_state['values']['translation']['status'], - 'language' => $form['#language'], - 'source' => $form['#source'], - ); - - $handler->setTranslation($translation, $form_state['values']); - - $form['#entity'] = (object) $form['#entity']; - entity_form_submit_build_entity($form['#entity_type'], $form['#entity'], $form, $form_state); - field_attach_presave($form['#entity_type'], $form['#entity']); - field_attach_update($form['#entity_type'], $form['#entity']); - - - $entity_info = entity_get_info($form['#entity_type']); - $id_key = $entity_info['entity keys']['id']; - entity_get_controller($form['#entity_type'])->resetCache(array($form['#entity']->{$id_key})); - - module_invoke_all('entity_translation_save', $form['#entity_type'], $form['#entity'], $form['#language']); - $form_state['redirect'] = "{$handler->getBasePath()}/translate"; -} - -/** - * Helper function to check if the path support is enabled. - */ -function _entity_translation_path_enabled(EntityTranslationHandlerInterface $handler) { - return $handler->isAliasEnabled() && module_exists('path'); -} - -/** - * Submit handler for the translation deletion. - */ -function entity_translation_edit_form_delete_submit($form, &$form_state) { - $form_state['redirect'] = "{$form['#handler']->getBasePath()}/translate/delete/{$form['#language']}"; -} - -/** * Translation deletion confirmation form. */ function entity_translation_delete_confirm($form, $form_state, $entity_type, $entity, $langcode) { @@ -502,7 +225,7 @@ function entity_translation_delete_confirm($form, $form_state, $entity_type, $en return confirm_form( $form, t('Are you sure you want to delete the @language translation of %label?', array('@language' => $languages[$langcode]->name, '%label' => $handler->getLabel())), - "{$handler->getBasePath()}/translate/edit/$langcode", + "{$handler->getBasePath()}/edit/$langcode", t('This action cannot be undone.'), t('Delete'), t('Cancel') @@ -514,13 +237,16 @@ function entity_translation_delete_confirm($form, $form_state, $entity_type, $en */ function entity_translation_delete_confirm_submit($form, &$form_state) { $handler = $form['#handler']; + $entity_type = $form['#entity_type']; + $entity = $form['#entity']; + $langcode = $form['#language']; - $handler->removeTranslation($form['#language']); - field_attach_update($form['#entity_type'], $form['#entity']); + // Remove the translation entry and the related fields. + $handler->removeTranslation($langcode); + field_attach_update($entity_type, $entity); - if (isset($_SESSION['entity_translation_source_language']) && $form['#language'] == $_SESSION['entity_translation_source_language']) { - unset($_SESSION['entity_translation_source_language']); - } + // Remove any existing path alias for the removed translation. + path_delete(array('source' => $handler->getViewPath(), 'language' => $langcode)); $form_state['redirect'] = "{$handler->getBasePath()}/translate"; } diff --git a/entity_translation.api.php b/entity_translation.api.php index 57481a5..57e8dac 100644 --- a/entity_translation.api.php +++ b/entity_translation.api.php @@ -2,37 +2,43 @@ /** * @file - * API documentation for Entity Translation module. + * API documentation for the Entity translation module. */ /** - * Allow modules to define their own translation info. + * Allows modules to define their own translation info. + * + * @param $types + * The available entity types. * * @return * An array of entity translation info to be merged into the entity info. * The translation info is an associative array that has to match the - * following sample structure: - * @code - * array( - * // Three nested sub-arrays keyed respectively by entity type and the - * // 'translation' keys: the first one is the key defined by the core - * // entity system, while the nested one registers Translation as a - * // translation handler. - * 'custom_entity' => array( - * 'translation' => array( - * 'translation' => array( - * 'class' => the entity class name, - * 'base path' => the base menu path to which attach the administration UI, - * 'access callback' => the access callback for the admin pages, - * 'access arguments' => the access arguments, - * // The edit form information, used to add the retranslate checkbox - * // to the entity edit form (if empty the edit form will not be - * // altered by Translation). - * 'edit form' => TRUE, - * ), - * ), - * ), - * ); + * following structure. Three nested sub-arrays keyed respectively by entity + * type, the 'translation' key and the 'entity_translation' key: the second + * one is the key defined by the core entity system while the third one + * registers Entity translation as a field translation handler. Elements: + * - class: The name of the translation handler class, which is used to handle + * the translation process. Defaults to 'EntityTranslationDefaultHandler'. + * - base path: The base menu router path to which attach the administration + * user interface. Defaults to "$entity_type/%$entity_type". + * - access callback: The access callback for the translation pages. Defaults + * to 'entity_translation_tab_access'. + * - access arguments: The access arguments for the translation pages. + * Defaults to array($entity_type). + * - view path: The menu router path to be used to view the entity. Defaults + * to the base path. + * - edit path: The menu router path to be used to edit the entity. Defaults + * to "$base_path/edit". + * - path wildcard: The menu router path wildcard identifying the entity. + * Defaults to "%$entity_type". + * - theme callback: The callback to be used to determine the translation + * theme. Defaults to 'variable_get'. + * - theme arguments: The arguments to be used to determine the translation + * theme. Defaults to array('admin_theme'). + * - edit form: The key to be used to retrieve the entity object from the form + * state array. An empty value prevents Entity translation from performing + * alterations to the entity form. Defaults to $entity_type. */ function hook_translation_info($types = NULL) { $info['custom_entity'] = array( @@ -42,28 +48,10 @@ function hook_translation_info($types = NULL) { 'base path' => 'custom_entity/%custom_entity', 'access callback' => 'custom_entity_tab_access', 'access arguments' => array(1), - 'edit form' => TRUE, + 'edit form' => 'custom_entity_form_key', ), ), ); return $info; } - -/** - * Allow modules to react on translation events. - * - * @param string $entity_type - * The type of entity; e.g. 'node' or 'user'. - * @param object $entity - * The entity to be translated. - * @param string $langcode - * The language code of the translation. - */ -function hook_entity_translation_save($entity_type, $entity, $langcode) { - $function = 'pathauto_' . $entity_type . '_update_alias'; - if (function_exists($function)) { - $options = array('language' => $langcode); - $function($entity, 'update', $options); - } -} diff --git a/entity_translation.info b/entity_translation.info index 8fca343..81c50c0 100644 --- a/entity_translation.info +++ b/entity_translation.info @@ -5,5 +5,7 @@ core = 7.x configure = admin/config/regional/entity_translation dependencies[] = locale files[] = includes/translation.handler.inc +files[] = includes/translation.handler.comment.inc files[] = includes/translation.handler.node.inc +files[] = includes/translation.handler.taxonomy_term.inc files[] = tests/entity_translation.test diff --git a/entity_translation.install b/entity_translation.install index 463a65c..ffff0b8 100644 --- a/entity_translation.install +++ b/entity_translation.install @@ -147,9 +147,10 @@ function entity_translation_disable() { function entity_translation_uninstall() { variable_del('translation_language_type'); variable_del('locale_field_language_fallback'); - variable_del('entity_translation_edit_form_info'); variable_del('entity_translation_entity_types'); variable_del('entity_translation_disabled_content_types'); + variable_del('entity_translation_languages_enabled'); + variable_del('entity_translation_shared_labels'); foreach (node_type_get_types() as $type => $object) { variable_del("entity_translation_node_metadata_$type"); diff --git a/entity_translation.module b/entity_translation.module index 467c110..3f2469b 100644 --- a/entity_translation.module +++ b/entity_translation.module @@ -7,6 +7,85 @@ module_load_include('inc', 'entity_translation', 'entity_translation.node'); + +/** + * Entity language callback. + * + * This callback changes the entity language from the actual one to the active + * form language. This overriding allows to obtain language dependent form + * widgets where multilingual values are supported (e.g. field or path alias + * widgets) even if the code was not originally written with supporting multiple + * values per language in mind. + * + * The main drawback of this approach is that code needing to access the actual + * language in the entity form build/validation/submit workflow cannot rely on + * the entity_language() function. On the other hand in these scenarios assuming + * the presence of Entity translation should be safe, thus being able to rely on + * the EntityTranslationHandlerInterface::getLanguage() method. + * + * @param $entity_type + * The the type of the entity. + * @param $entity + * The entity whose language has to be returned. + * + * @return + * A valid language code. + */ +function entity_translation_language($entity_type, $entity) { + // If a form has been post, we need to check its state to verify if any form + // translation handler is stored there. This is mainly needed when responding + // to an AJAX request where the form language cannot be set from the page + // callback. + $handler = entity_translation_current_form_get_handler(); + + // Make sure we always have a translation handler instance available. + if (empty($handler)) { + $handler = entity_translation_get_handler($entity_type, $entity); + } + // If a translation handler associated to the current form is found, we need + // to update the wrapped entity. This way submitted values will be picked up. + else { + $langcode = $handler->getLanguage(); + $handler->setEntity($entity); + $submitted_langcode = $handler->getLanguage(); + // If the entity language has changed we are editing tha original values. In + // this case we need to update the current form language with the submitted + // one. + if ($submitted_langcode != $langcode) { + $handler->setFormLanguage($submitted_langcode); + } + } + + $langcode = $handler->getFormLanguage(); + return !empty($langcode) ? $langcode : $handler->getLanguage(); +} + +/** + * Returns the translation handler associated to the currently submitted form. + * + * @return EntityTranslationHandlerInterface + * A translation handler instance if available, FALSE oterwise. + */ +function entity_translation_current_form_get_handler() { + $handler = FALSE; + + if (!empty($_POST['form_build_id'])) { + $form_state = form_state_defaults(); + $form = form_get_cache($_POST['form_build_id'], $form_state); + $handler = entity_translation_entity_form_get_handler($form, $form_state); + } + + return $handler; +} + +/** + * Helper function. Determines whether the given entity type is translatable. + */ +function entity_translation_enabled($entity_type, $skip_handler = FALSE) { + $enabled_types = variable_get('entity_translation_entity_types', array()); + return !empty($enabled_types[$entity_type]) && ($skip_handler || field_has_translation_handler($entity_type, 'entity_translation')); +} + /** * Implements hook_language_type_info_alter(). */ @@ -22,7 +101,7 @@ function entity_translation_translation_info($types = NULL) { 'comment' => array( 'translation' => array( 'entity_translation' => array( - 'edit form' => FALSE, + 'class' => 'EntityTranslationCommentHandler', ), ), ), @@ -30,7 +109,6 @@ function entity_translation_translation_info($types = NULL) { 'translation' => array( 'entity_translation' => array( 'class' => 'EntityTranslationNodeHandler', - 'alias' => TRUE, 'access callback' => 'entity_translation_node_tab_access', 'access arguments' => array(1), ), @@ -39,13 +117,13 @@ function entity_translation_translation_info($types = NULL) { 'taxonomy_term' => array( 'translation' => array( 'entity_translation' => array( + 'class' => 'EntityTranslationTaxonomyTermHandler', 'base path' => 'taxonomy/term/%taxonomy_term', - 'alias' => TRUE, + 'edit form' => 'term', ), ), ), - 'user' => array( - ), + 'user' => array(), ); return isset($types) ? array_intersect_key($info, $types) : $info; @@ -74,6 +152,8 @@ function entity_translation_entity_info_alter(&$entity_info) { $entity_info[$entity_type]['translation']['entity_translation'] += array('class' => 'EntityTranslationDefaultHandler'); if (entity_translation_enabled($entity_type, TRUE)) { + $entity_info[$entity_type]['language callback'] = 'entity_translation_language'; + // If no base path is provided we default to the common "node/%node" // pattern. if (!isset($entity_info[$entity_type]['translation']['entity_translation']['base path'])) { @@ -103,9 +183,14 @@ function entity_translation_entity_info_alter(&$entity_info) { 'access arguments' => array($entity_type), 'theme callback' => 'variable_get', 'theme arguments' => array('admin_theme'), - 'edit form' => TRUE, ); + // Interpret a TRUE value for the 'edit form' key as the default value. + $et_info = &$entity_info[$entity_type]['translation']['entity_translation']; + if (!isset($et_info['edit form']) || $et_info['edit form'] === TRUE) { + $et_info['edit form'] = $entity_type; + } + $entity_info[$entity_type]['entity keys'] += array( 'translations' => 'translations', ); @@ -114,21 +199,21 @@ function entity_translation_entity_info_alter(&$entity_info) { } /** - * Helper function to determine if the given entity type is translatable. - */ -function entity_translation_enabled($entity_type, $skip_handler = FALSE) { - $enabled_types = variable_get('entity_translation_entity_types', array()); - return - !empty($enabled_types[$entity_type]) && - ($skip_handler || field_has_translation_handler($entity_type, 'entity_translation')); -} - -/** - * Implments hook_menu(). + * Implements hook_menu(). */ function entity_translation_menu() { $items = array(); + $items['admin/config/regional/entity_translation'] = array( + 'title' => 'Entity translation', + 'description' => 'Configure which entities can be translated and enable or disable language falback.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('entity_translation_admin_form'), + 'access arguments' => array('administer entity translation'), + 'file' => 'entity_translation.admin.inc', + 'module' => 'entity_translation', + ); + $items['admin/config/regional/entity_translation/translatable/%'] = array( 'title' => 'Confirm change in translatability.', 'description' => 'Confirm page for changing field translatability.', @@ -145,14 +230,15 @@ function entity_translation_menu() { * Implements hook_menu_alter(). */ function entity_translation_menu_alter(&$items) { - $backup = array(); - // If entity translation information is being rebuilt we must not proceed to // avoid recursion. if (!empty($GLOBALS['entity_translation_info_building'])) { return; } + $backup = array(); + $languages = entity_translation_languages(); + // Create tabs for all possible entity types. foreach (entity_get_info() as $entity_type => $info) { // Menu is rebuilt while determining entity translation base paths and @@ -185,54 +271,226 @@ function entity_translation_menu_alter(&$items) { 'weight' => 2, ) + $item; - $items["$path/translate/list"] = array( - 'title' => 'List', - 'type' => MENU_DEFAULT_LOCAL_TASK, - 'weight' => 0, - ); + $et_info = $info['translation']['entity_translation']; + $edit_path = $et_info['edit path']; - $items["$path/translate/add/%entity_translation_language/%entity_translation_language"] = array( - 'title' => 'Add', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('entity_translation_edit_form', $entity_type, $entity_position, $language_position, $source_position), - 'type' => MENU_LOCAL_TASK, - 'weight' => 1, - ) + $item; + if (isset($items[$edit_path])) { + // If the edit path is a default local task we need to find the parent + // item. + $edit_path_split = explode('/', $edit_path); + do { + $edit_form_item = &$items[implode('/', $edit_path_split)]; + array_pop($edit_path_split); + } + while (!empty($edit_form_item['type']) && $edit_form_item['type'] == MENU_DEFAULT_LOCAL_TASK); - $items["$path/translate/edit/%entity_translation_language"] = array( - 'title' => 'Edit', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('entity_translation_edit_form', $entity_type, $entity_position, $language_position), - 'access callback' => 'entity_translation_edit_access', - 'access arguments' => array_merge(array($entity_type, $entity_position, $language_position, $item['access callback']), $item['access arguments']), - 'type' => MENU_LOCAL_TASK, - 'weight' => 1, - ) + $item; + // Make the "Translate" local task follow the "Edit" one when possibile. + if (isset($edit_form_item['weight'])) { + $items["$path/translate"]['weight'] = $edit_form_item['weight'] + 1; + } - $items["$path/translate/delete/%entity_translation_language"] = array( - 'title' => 'Delete', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('entity_translation_delete_confirm', $entity_type, $entity_position, $language_position), - ) + $item; + // Replace the main edit callback with our proxy implementation to set + // form language to the current language and check access. + $entity_position = count(explode('/', $et_info['base path'])) - 1; + $edit_position = count(explode('/', $edit_path)) - 1; + $original_item = $edit_form_item; + $args = array($entity_type, $entity_position, FALSE, $original_item); + $edit_form_item['page callback'] = 'entity_translation_edit_page'; + $edit_form_item['page arguments'] = array_merge($args, $original_item['page arguments']); + $edit_form_item['access callback'] = 'entity_translation_edit_access'; + $edit_form_item['access arguments'] = array_merge($args, $original_item['access arguments']); + + // Edit translation callback. + $translation_position = $edit_position + 1; + $args = array($entity_type, $entity_position, $translation_position, $original_item); + $items["$edit_path/%entity_translation_language"] = array( + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'title callback' => 'entity_translation_edit_title', + 'title arguments' => array($translation_position), + 'page callback' => 'entity_translation_edit_page', + 'page arguments' => array_merge($args, $original_item['page arguments']), + 'access callback' => 'entity_translation_edit_access', + 'access arguments' => array_merge($args, $original_item['access arguments']), + ) + // We need to inherit the remaining menu item keys, mostly 'module' and + // 'file' to keep ajax callbacks working (see drupal_retrieve_form() and + // form_get_cache()). + + $original_item; + + // Add translation callback. + $add_path = "$edit_path/add/%entity_translation_language/%entity_translation_language"; + $source_position = $edit_position + 2; + $items[$add_path] = array( + 'title callback' => 'Add translation', + 'page callback' => 'entity_translation_add_page', + 'page arguments' => array_merge(array($entity_type, $entity_position, $source_position, $source_position + 1, $original_item), $original_item['page arguments']), + 'type' => MENU_LOCAL_TASK, + 'access callback' => 'entity_translation_add_access', + 'access arguments' => array_merge(array($entity_type, $entity_position, $source_position, $source_position + 1, $original_item), $original_item['access arguments']), + ) + $original_item; + + // Delete translation callback. + $items["$path/translate/delete/%entity_translation_language"] = array( + 'title' => 'Delete', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('entity_translation_delete_confirm', $entity_type, $entity_position, $language_position), + ) + $item; + } } } - $items['admin/config/regional/entity_translation'] = array( - 'title' => 'Entity translation', - 'description' => 'Configure which entities can be translated and enable or disable language falback.', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('entity_translation_admin_form'), - 'access arguments' => array('administer entity translation'), - 'file' => 'entity_translation.admin.inc', - 'module' => 'entity_translation', - ); - // Node-specific menu alterations. entity_translation_node_menu_alter($items, $backup); +} - return $items; +/** + * Title callback. + */ +function entity_translation_edit_title($langcode) { + $languages = entity_translation_languages(); + return isset($languages[$langcode]) ? t($languages[$langcode]->name) : ''; +} + +/** + * Page callback. + */ +function entity_translation_edit_page() { + $args = func_get_args(); + $entity_type = array_shift($args); + $entity = array_shift($args); + $langcode = array_shift($args); + $edit_form_item = array_shift($args); + + // Set the current form language. + $handler = entity_translation_get_handler($entity_type, $entity); + $translations = $handler->getTranslations(); + $langcode = entity_translation_form_language($langcode, $handler); + $handler->setFormLanguage($langcode); + + // Display the entity edit form. + return _entity_translation_callback($edit_form_item['page callback'], $args, $edit_form_item); +} + +/** + * Access callback. + */ +function entity_translation_edit_access() { + $args = func_get_args(); + $entity_type = array_shift($args); + $entity = array_shift($args); + $langcode = array_shift($args); + + $handler = entity_translation_get_handler($entity_type, $entity); + $translations = $handler->getTranslations(); + $langcode = entity_translation_form_language($langcode, $handler); + + // The user must be explicitly allowed to access the original values. + if (!$handler->getTranslationAccess($langcode)) { + return FALSE; + } + + // If the translation exists or no translation was specified, we can show the + // corresponding local task. If translations have not been initialized yet, we + // need to grant access to the user. + if (empty($translations->data) || isset($translations->data[$langcode])) { + // Check that the requested language is actually accessible. If the entity + // is language neutral we need to let editors access it. + $enabled_languages = entity_translation_languages($entity_type, $entity); + if (isset($enabled_languages[$langcode]) || $langcode == LANGUAGE_NONE) { + $edit_form_item = array_shift($args); + return _entity_translation_callback($edit_form_item['access callback'], $args, $edit_form_item); + } + } + + return FALSE; +} + +/** + * Determines the current form language. + * + * Based on the requested language and the translations available for the entity + * being edited, determines the active form language. This takes into account + * language fallback rules so that the translation being edited matches the one + * being viewed. + * + * @param $langcode + * The requested language code. + * @param EntityTranslationHandlerInterface $handler + * A translation handler instance. + * + * @return + * A valid language code. + */ +function entity_translation_form_language($langcode, $handler) { + if (empty($langcode)) { + $langcode = $GLOBALS['language_content']->language; + } + + $translations = $handler->getTranslations(); + $fallback = language_fallback_get_candidates(); + while (!empty($langcode) && !isset($translations->data[$langcode])) { + $langcode = array_shift($fallback); + } + + // If no translation is available fall back to the entity language. + return !empty($langcode) ? $langcode : $handler->getLanguage(); +} + +/** + * Access callback. + */ +function entity_translation_add_access() { + $args = func_get_args(); + $entity_type = array_shift($args); + $entity = array_shift($args); + $source = array_shift($args); + $langcode = array_shift($args); + + $handler = entity_translation_get_handler($entity_type, $entity); + $translations = $handler->getTranslations(); + + // If the translation does not exist we can show the tab. + if (!isset($translations->data[$langcode]) && $langcode != $source) { + // Check that the requested language is actually accessible. + $enabled_languages = entity_translation_languages($entity_type, $entity); + if (isset($enabled_languages[$langcode])) { + $edit_form_item = array_shift($args); + return _entity_translation_callback($edit_form_item['access callback'], $args, $edit_form_item); + } + } + + return FALSE; } +/** + * Page callback. + */ +function entity_translation_add_page() { + $args = func_get_args(); + $entity_type = array_shift($args); + $entity = array_shift($args); + $source = array_shift($args); + $langcode = array_shift($args); + $edit_form_item = array_shift($args); + + $handler = entity_translation_get_handler($entity_type, $entity); + $handler->setFormLanguage($langcode); + $handler->setSourceLanguage($source); + + // Display the entity edit form. + return _entity_translation_callback($edit_form_item['page callback'], $args, $edit_form_item); +} + +/** + * Helper function. Proxies a callback call including any needed file. + */ +function _entity_translation_callback($callback, $args, $info = array()) { + if (isset($info['file'])) { + $path = isset($info['file path']) ? $info['file path'] : drupal_get_path('module', $info['module']); + include_once DRUPAL_ROOT . '/' . $path . '/' . $info['file']; + } + return call_user_func_array($callback, $args); +} /** * Implements hook_admin_paths(). @@ -244,6 +502,7 @@ function entity_translation_admin_paths() { $base_path = preg_replace('|%[^/]*|', '*', $info['translation']['entity_translation']['base path']); $paths["$base_path/translate"] = TRUE; $paths["$base_path/translate/*"] = TRUE; + $paths["$base_path/edit/*"] = TRUE; } } return $paths; @@ -257,25 +516,11 @@ function entity_translation_tab_access($entity_type) { } /** - * Access callback. - */ -function entity_translation_edit_access($entity_type, $entity, $langcode) { - $translations = entity_translation_get_handler($entity_type, $entity)->getTranslations(); - // If a translations for the given language does not exist we cannot edit it. - if (!isset($translations->data[$langcode])) { - return FALSE; - } - // Invoke the actual callback with its arguments. - $args = func_get_args(); - return call_user_func_array($args[3], array_slice($args, 4)); -} - -/** * Menu loader callback. */ -function entity_translation_language_load($langcode) { - $enabled_languages = field_content_languages(); - return in_array($langcode, $enabled_languages) ? $langcode : FALSE; +function entity_translation_language_load($langcode, $entity_type = NULL, $entity = NULL) { + $enabled_languages = entity_translation_languages($entity_type, $entity); + return isset($enabled_languages[$langcode]) ? $langcode : FALSE; } /** @@ -291,10 +536,6 @@ function entity_translation_menu_entity_load($entity_id, $entity_type) { */ function entity_translation_permission() { $permission = array( - 'translate any entity' => array( - 'title' => t('Translate any entity'), - 'description' => t('Translate field content for any fieldable entity.'), - ), 'administer entity translation' => array( 'title' => t('Administer entity translation'), 'description' => t('Select which entities can be translated.'), @@ -303,16 +544,30 @@ function entity_translation_permission() { 'title' => t('Toggle field translatability'), 'description' => t('Toggle translatability of fields performing a bulk update.'), ), + 'edit translation shared fields' => array( + 'title' => t('Edit shared fields'), + 'description' => t('Edit fields shared between translations on the translation form.'), + ), + 'edit original values' => array( + 'title' => t('Edit original values'), + 'description' => t('Access the entity edit form in the original language.'), + ), + 'translate any entity' => array( + 'title' => t('Translate any entity'), + 'description' => t('Translate field content for any fieldable entity.'), + ), ); + foreach (entity_get_info() as $entity_type => $info) { if ($info['fieldable']) { $label = t($info['label']); $permission["translate $entity_type entities"] = array( 'title' => t('Translate entities of type @type', array('@type' => $label)), - 'description' => t('Translate field content for entities of type @type', array('@type' => $label)), + 'description' => t('Translate field content for entities of type @type.', array('@type' => $label)), ); } } + return $permission; } @@ -324,6 +579,9 @@ function entity_translation_theme() { 'entity_translation_unavailable' => array( 'variables' => array('element' => NULL), ), + 'entity_translation_language_tabs' => array( + 'render element' => 'element', + ), ); } @@ -458,48 +716,341 @@ function entity_translation_field_attach_delete($entity_type, $entity) { } /** - * Implements hook_form_alter(). + * Implementation of hook_field_attach_form(). */ -function entity_translation_form_alter(&$form, $form_state) { - if (($info = entity_translation_edit_form_info($form)) && entity_translation_enabled($info['entity type'])) { - $handler = entity_translation_get_handler($info['entity type'], $info['entity']); +function entity_translation_field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode) { + // Skip recursing into the source form. + if (empty($form['#entity_translation_source_form']) && ($handler = entity_translation_entity_form_get_handler($form, $form_state))) { + $form_langcode = $handler->getFormLanguage(); $translations = $handler->getTranslations(); + $update_langcode = $form_langcode && ($form_langcode != $langcode); + $source = $handler->getSourceLanguage(); + $new_translation = !isset($translations->data[$form_langcode]); + + // If we are creating a new translation we need to retrieve form elements + // populated with the source language values, but only if form is not being + // rebuilt. In this case source values have already been populated, so we + // need to preserve possible changes. There might be situations, e.g. ajax + // calls, where the form language has not been properly initialized before + // calling field_attach_form(). In this case we need to rebuild the form + // with the correct form language and replace the field elements with the + // correct ones. + if ($update_langcode || ($source && !isset($translations->data[$form_langcode]) && isset($translations->data[$source]))) { + list(, , $bundle) = entity_extract_ids($entity_type, $entity); - if (!empty($translations->data) && !entity_translation_node($info['entity type'], $info['entity'])) { - $form['translation'] = array( - '#type' => 'fieldset', - '#title' => t('Translation'), - '#collapsible' => TRUE, - '#tree' => TRUE, - ); + foreach (field_info_instances($entity_type, $bundle) as $instance) { + $field_name = $instance['field_name']; + $field = field_info_field($field_name); + + // If we are creating a new translation we have to change the form item + // language information from source to target language, this way the + // user can find the form items already populated with the source values + // while the field form element holds the correct language information. + if ($field['translatable']) { + $form[$field_name]['#field_name'] = $field_name; + $form[$field_name]['#source'] = $update_langcode ? $form_langcode : $source; + $form[$field_name]['#previous'] = NULL; + + // If we are updating the form language we need to make sure that the + // wrong language is unset and the right one is stored in the field + // element (see entity_translation_prepare_element()). + if ($update_langcode) { + $form[$field_name]['#previous'] = $form[$field_name]['#language']; + $form[$field_name]['#language'] = $form_langcode; + } - $form['translation']['retranslate'] = array( - '#type' => 'checkbox', - '#title' => t('Flag translations as outdated'), - '#default_value' => 0, - '#description' => t('If you made a significant change, which means translations should be updated, you can flag all translations of this post as outdated. This will not change any other property of those posts, like whether they are published or not.'), - ); + // Swap default values during form processing to avoid recursion. We + // try to act before any other callback so that the correct values are + // already in place for them. + if (!isset($form[$field_name]['#process'])) { + $form[$field_name]['#process'] = array(); + } + array_unshift($form[$field_name]['#process'], 'entity_translation_prepare_element'); + } + } + } + + // Handle fields shared between translations when there is at least one + // translation available or a new one is being created. + if (!$handler->isNewEntity() && ($new_translation || count($translations->data) > 1)) { + list(, , $bundle) = entity_extract_ids($entity_type, $entity); + + // Shared fields are visible if we are either editing the source node, or + // if the user has the respective permission. + $is_source = $langcode == $handler->getLanguage(); + $shared_fields_visible = $is_source || user_access('edit translation shared fields'); - array_unshift($form['#submit'], 'entity_translation_edit_form_submit'); + foreach (field_info_instances($entity_type, $bundle) as $instance) { + $field_name = $instance['field_name']; + $field = field_info_field($field_name); - // Node specific alterations. - if ($info['entity type'] == 'node') { - entity_translation_node_alter_form($form, $form_state, $handler); + if ($shared_fields_visible) { + // Add visual clues about translatability. + if (!isset($form[$field_name]['#process'])) { + $form[$field_name]['#process'] = array(); + } + $form[$field_name]['#field_name'] = $field_name; + array_unshift($form[$field_name]['#process'], 'entity_translation_element_translatability_clue'); + } + // Hide shared fields. + elseif (!$field['translatable']) { + $form[$field_name]['#access'] = FALSE; + } } } } } /** - * Submit handler for the entity edit form. + * Form element process callback. + */ +function entity_translation_prepare_element($element, &$form_state) { + $source_form = &drupal_static(__FUNCTION__, array()); + $form = $form_state['complete form']; + $build_id = $form['#build_id']; + $source = $element['#source']; + + if (!isset($source_form[$build_id][$source])) { + $source_form[$build_id][$source] = array('#entity_translation_source_form' => TRUE); + $source_form_state = $form_state; + $info = entity_translation_edit_form_info($form, $form_state); + field_attach_form($info['entity type'], $info['entity'], $source_form[$build_id][$source], $source_form_state, $source); + } + + $langcode = $element['#language']; + $field_name = $element['#field_name']; + + // If we are creating a new translation we have to change the form item + // language information from source to target language, this way the user can + // find the form items already populated with the source values while the + // field form element holds the correct language information. + if (isset($source_form[$build_id][$source][$field_name][$source])) { + $element[$langcode] = $source_form[$build_id][$source][$field_name][$source]; + entity_translation_form_element_language_replace($element, $source, $langcode); + unset($element[$element['#previous']]); + } + + return $element; +} + +/** + * Helper function. Recursively replaces the source language with the given one. + */ +function entity_translation_form_element_language_replace(&$element, $source, $langcode) { + // Iterate through the form structure recursively. + foreach (element_children($element) as $key) { + entity_translation_form_element_language_replace($element[$key], $source, $langcode); + } + + // Replace specific occurrences of the source language with the target + // language. + foreach (element_properties($element) as $key) { + if ($key === '#language') { + $element[$key] = $langcode; + } + + if ($key === '#parents' || $key === '#field_parents') { + foreach ($element[$key] as $delta => $value) { + if ($value === $source) { + $element[$key][$delta] = $langcode; + } + } + } + } +} + +/** + * Adds visual clues about the translatability of a field to the given element. + * + * Field titles are appended with the string "Shared" for fields which are + * shared between different translations. Moreover fields receive a CSS class to + * distinguish between translatable and shared fields. + */ +function entity_translation_element_translatability_clue($element, &$form_state, $form) { + $languages = language_list(); + + $field_name = $element['#field_name']; + $field = field_info_field($field_name); + $field_language = $element['#language']; + + // Append language to element title. + if (variable_get('entity_translation_shared_labels', TRUE)) { + $suffix = $field['translatable'] ? '' : ' (' . t('shared field') . ')'; + _entity_translation_element_title_append($element, $suffix); + } + + // Add CSS class names. + if (!isset($element['#attributes'])) { + $element['#attributes'] = array(); + } + if (!isset($element['#attributes']['class'])) { + $element['#attributes']['class'] = array(); + } + $element['#attributes']['class'][] = 'entity-translation-' . ($field['translatable'] ? 'field-translatable' : 'field-shared'); + + return $element; +} + +/** + * Appends the given $suffix string to the title of the given form element. + * + * If the given element does not have a #title attribute, the function is + * recursively applied to child elements. + */ +function _entity_translation_element_title_append(&$element, $suffix) { + static $fapi_title_elements; + + // Elements which can have a #title attribute according to FAPI Reference. + if (!isset($fapi_title_elements)) { + $fapi_title_elements = array_flip(array('checkbox', 'checkboxes', 'date', 'fieldset', 'file', 'item', 'password', 'password_confirm', 'radio', 'radios', 'select', 'text_format', 'textarea', 'textfield', 'weight')); + } + + // Update #title attribute for all elements that are allowed to have a #title + // attribute according to the Form API Reference. The reason for this check + // is because some elements have a #title attribute even though it is not + // rendered, e.g. field containers. + if (isset($element['#type']) && isset($fapi_title_elements[$element['#type']]) && isset($element['#title'])) { + $element['#title'] .= $suffix; + } + // If the current element does not have a (valid) title, try child elements. + elseif ($children = element_children($element)) { + foreach ($children as $delta) { + _entity_translation_element_title_append($element[$delta], $suffix); + } + } + // If there are no children, fall back to the current #title attribute if it + // exists. + elseif (isset($element['#title'])) { + $element['#title'] .= $suffix; + } +} + +/** + * Implements hook_form_alter(). + */ +function entity_translation_form_alter(&$form, &$form_state) { + if (($handler = entity_translation_entity_form_get_handler($form, $form_state)) && !$handler->isNewEntity()) { + $handler->entityForm($form, $form_state); + } +} + +/** + * Submit handler for the source language selector. + */ +function entity_translation_entity_form_source_language_submit($form, &$form_state) { + $handler = entity_translation_entity_form_get_handler($form, $form_state); + $langcode = $form_state['values']['source_language']['language']; + $path = "{$handler->getEditPath()}/add/$langcode/{$handler->getFormLanguage()}"; + $form_state['redirect'] = array('path' => $path); + $languages = language_list(); + drupal_set_message(t('Source translation set to: %language', array('%language' => t($languages[$langcode]->name)))); +} + +/** + * Submit handler for the translation deletion. + */ +function entity_translation_entity_form_delete_translation_submit($form, &$form_state) { + $handler = entity_translation_entity_form_get_handler($form, $form_state); + $form_state['redirect'] = "{$handler->getBasePath()}/translate/delete/{$handler->getFormLanguage()}"; +} + +/** + * Validation handler for the entity edit form. + */ +function entity_translation_entity_form_validate($form, &$form_state) { + $handler = entity_translation_entity_form_get_handler($form, $form_state); + $handler->entityFormValidate($form, $form_state); +} + +/** + * Submit handler for the entity deletion. + */ +function entity_translation_entity_form_submit($form, &$form_state) { + if ($form_state['clicked_button']['#value'] == t('Delete')) { + $handler = entity_translation_entity_form_get_handler($form, $form_state); + if (count($handler->getTranslations()->data) > 1) { + $info = entity_get_info($form['#entity_type']); + drupal_set_message(t('This will delete all the @entity_type translations.', array('@entity_type' => drupal_strtolower($info['label']))), 'warning'); + } + } +} + +/** + * Implementation of hook_field_attach_submit(). * * Mark translations as outdated if the submitted value is true. */ -function entity_translation_edit_form_submit($form, &$form_state) { - $info = entity_translation_edit_form_info($form); - $handler = entity_translation_get_handler($info['entity type'], $info['entity']); - $outdated = !empty($form_state['values']['translation']['retranslate']); - $handler->setOutdated($outdated); +function entity_translation_field_attach_submit($entity_type, $entity, $form, &$form_state) { + if ($handler = entity_translation_entity_form_get_handler($form, $form_state)) { + // Update the wrapped entity with the submitted values. + $handler->setEntity($entity); + $handler->entityFormSubmit($form, $form_state); + } +} + +/** + * Implements hook_menu_local_tasks_alter(). + */ +function entity_translation_menu_local_tasks_alter(&$data, $router_item, $root_path) { + // When displaying the main edit form, we need to craft an additional level of + // local tasks for each available translation. + $handler = entity_translation_get_handler(); + if (!empty($handler) && $handler->getLanguage() != LANGUAGE_NONE && ($form_langcode = $handler->getFormLanguage()) && drupal_multilingual()) { + $handler->localTasksAlter($data, $router_item, $root_path); + } +} + +/** + * Preprocess variables for 'page.tpl.php'. + */ +function entity_translation_preprocess_page(&$variables) { + if (!empty($variables['tabs']['#secondary'])) { + $language_tabs = array(); + + foreach ($variables['tabs']['#secondary'] as $index => $tab) { + if (!empty($tab['#language_tab'])) { + $language_tabs[] = $tab; + unset($variables['tabs']['#secondary'][$index]); + } + } + + if (!empty($language_tabs)) { + if (count($variables['tabs']['#secondary']) <= 1) { + $variables['tabs']['#secondary'] = $language_tabs; + } + else { + // If secondary tabs are already defined we need to add another level + // and wrap it so that it will be positioned on its own row. + $variables['tabs']['#secondary']['#language_tabs'] = $language_tabs; + $variables['tabs']['#secondary']['#pre_render']['entity_translation'] = 'entity_translation_language_tabs_render'; + } + } + } +} + +/** + * Pre render callback. + * + * Appends the language tabs to the current local tasks area. + */ +function entity_translation_language_tabs_render($element) { + $build = array( + '#theme' => 'menu_local_tasks', + '#theme_wrappers' => array('entity_translation_language_tabs'), + '#secondary' => $element['#language_tabs'], + '#attached' => array( + 'css' => array(drupal_get_path('module', 'entity_translation') . '/entity-translation.css'), + ), + ); + $element['#suffix'] .= drupal_render($build); + return $element; +} + +/** + * Theme wrapper for the entity translation language tabs. + */ +function theme_entity_translation_language_tabs($variables) { + return '