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 '
' . $variables['element']['#children'] . '
'; } /** @@ -548,18 +1099,36 @@ function entity_translation_form_field_ui_field_edit_form_alter(&$form, $form_st * Translation handler factory. * * @param $entity_type - * The type of $entity; e.g. 'node' or 'user'. + * (optional) The type of $entity; e.g. 'node' or 'user'. * @param $entity - * The entity to be translated. + * (optional) The entity to be translated. * @param $update - * Instances are statically cached: if this is TRUE the wrapped entity will - * be replaced by the passed one. + * (optional) Instances are statically cached: if this is TRUE the wrapped + * entity will be replaced by the passed one. * - * @return - * A class implementing the EntityTranslationHandler interface. + * @return EntityTranslationHandlerInterface + * A class implementing EntityTranslationHandlerInterface. */ -function entity_translation_get_handler($entity_type, $entity, $update = FALSE) { - $handlers = &drupal_static(__FUNCTION__); +function entity_translation_get_handler($entity_type = NULL, $entity = NULL, $update = FALSE) { + static $drupal_static_fast; + if (!isset($drupal_static_fast['handlers'])) { + $drupal_static_fast['handlers'] = &drupal_static(__FUNCTION__, array()); + } + $handlers = &$drupal_static_fast['handlers']; + + // Workaround the lack of a context object. + if (empty($entity)) { + if (isset($handlers[$entity_type]['#current'])) { + return $handlers[$entity_type]['#current']; + } + elseif (empty($entity_type) && isset($handlers['#current']['#current'])) { + return $handlers['#current']['#current']; + } + else { + return NULL; + } + } + list($entity_id) = entity_extract_ids($entity_type, $entity); if (!isset($handlers[$entity_type][$entity_id])) { @@ -580,175 +1149,93 @@ function entity_translation_get_handler($entity_type, $entity, $update = FALSE) $handlers[$entity_type][$entity_id]->setEntity($entity); } + $handlers[$entity_type]['#current'] = $handlers[$entity_type][$entity_id]; + $handlers['#current']['#current'] = $handlers[$entity_type][$entity_id]; return $handlers[$entity_type][$entity_id]; } /** - * Return an array of edit form info as defined in hook_translation_info(). + * Returns the translation handler wrapping the entity being edited. * * @param $form - * The entity edit form. + * The entity form. + * @param $form_state + * A keyed array containing the current state of the form. * - * @return - * An edit form info array containing the entity to be translated in the - * 'entity' key. + * @return EntityTranslationHandlerInterface + * A class implementing EntityTranslationHandlerInterface. */ -function entity_translation_edit_form_info($form) { - if (isset($form['#entity_type']) && isset($form['#' . $form['#entity_type']])) { - $entity_info = entity_get_info(); - if (!empty($entity_info[$form['#entity_type']]['translation']['entity_translation']['edit form'])) { - return array( - 'entity type' => $form['#entity_type'], - 'entity' => $form['#' . $form['#entity_type']], - ); +function entity_translation_entity_form_get_handler($form, &$form_state) { + $handler = FALSE; + + if (empty($form_state['storage']['entity_translation']['handler'])) { + $info = entity_translation_edit_form_info($form, $form_state); + if (entity_translation_enabled($info['entity type']) && !entity_translation_node($info['entity type'], $info['entity'])) { + $handler = entity_translation_get_handler($info['entity type'], $info['entity']); + $form_state['storage']['entity_translation']['handler'] = $handler; } } - return FALSE; + else { + $handler = $form_state['storage']['entity_translation']['handler']; + } + + return $handler; } /** - * Check if an entity translation is accessible. + * Returns an array of edit form info as defined in hook_translation_info(). * - * @param $translation - * An array representing an entity translation. + * @param $form + * The entity edit form. + * @param $form_state + * The entity edit form state. * * @return - * TRUE if the current user is allowed to view the translation. - */ -function entity_translation_access($entity_type, $translation) { - return $translation['status'] || user_access('translate any entity') || user_access("translate $entity_type entities"); -} - -/** - * Implements hook_entity_translation_save(). - * - * @param $entity_type - * The type of entity; e.g. 'node' or 'user'. - * @param $entity - * The entity to be translated. - * @param $langcode - * The language code of the translation. + * An edit form info array containing the entity to be translated in the + * 'entity' key. */ -function entity_translation_entity_translation_save($entity_type, $entity, $langcode) { - $handler = entity_translation_get_handler($entity_type, $entity); - - // Update URL alias if applicable. - // The user needs the permission and pathauto has to be disabled. - if (_entity_translation_path_enabled($handler) && empty($entity->path['pathauto']) && (user_access('create url aliases') || user_access('administer url aliases'))) { - if (!empty($entity->path['pid']) && empty($entity->path['alias'])) { - path_delete($entity->path['pid']); - } - if (!empty($entity->path['alias'])) { - path_save($entity->path); +function entity_translation_edit_form_info($form, $form_state) { + $info = FALSE; + + if (isset($form['#entity_type'])) { + $entity_info = entity_get_info($form['#entity_type']); + if (!empty($entity_info['translation']['entity_translation']['edit form'])) { + $entity_key = $entity_info['translation']['entity_translation']['edit form']; + if (isset($form_state[$entity_key])) { + $info = array( + 'entity type' => $form['#entity_type'], + 'entity' => (object) $form_state[$entity_key], + ); + } } } - // Trigger pathauto if module is available. - if (module_exists('pathauto') && !empty($entity->path['pathauto'])) { - module_load_include('module', 'pathauto'); - $function = 'pathauto_' . $entity_type . '_update_alias'; - if (function_exists($function)) { - $options = array('language' => $langcode); - $function($entity, 'update', $options); - } - } + return $info; } /** - * Implements hook_pathauto(). + * Checks whether an entity translation is accessible. * - * Create own bulk creation callback to deal with node aliases for translations. - */ -function entity_translation_pathauto($op) { - switch ($op) { - case 'settings': - $settings = array(); - $settings['module'] = 'node'; - $settings['token_type'] = 'node'; - $settings['groupheader'] = t('Content paths'); - $settings['patterndescr'] = t('Default path pattern (applies to all content types with blank patterns below)'); - $settings['patterndefault'] = 'content/[node:title]'; - $settings['batch_update_callback'] = 'entity_translation_pathauto_bulk_update_batch_process'; - $settings['batch_file'] = drupal_get_path('module', 'entity_translation') . '/entity_translation.module'; - $settings['patternitems'] = array(); - return (object) $settings; - default: - break; - } -} - -/** - * Implements hook_form_FORM_ID_alter(). + * @param $translation + * An array representing an entity translation. * - * Remove the option for nodes - they are handled by this module now. + * @return + * TRUE if the current user is allowed to view the translation. */ -function entity_translation_form_pathauto_bulk_update_form_alter(&$form, $form_state, $form_id) { - unset($form['update']['#options']['node_pathauto_bulk_update_batch_process']); +function entity_translation_access($entity_type, $translation) { + return $translation['status'] || user_access('translate any entity') || user_access("translate $entity_type entities"); } /** - * Callback for bulk alias creation. - * - * Make sure that every translation of a node has the appropriate alias. - * This function replaces node_pathauto_bulk_update_batch_process(). - * - * @param array $context + * Returns the set of languages available for translations. */ -function entity_translation_pathauto_bulk_update_batch_process(&$context) { - if (!isset($context['sandbox']['current'])) { - $context['sandbox']['count'] = 0; - $context['sandbox']['current'] = 0; +function entity_translation_languages($entity_type = NULL, $entity = NULL) { + if (isset($entity) && $entity_type == 'node' && module_exists('i18n_node')) { + // @todo Inherit i18n language settings. } - - $query = db_select('node', 'n'); - - if (module_exists('entity_translation') && function_exists('entity_translation_enabled') && entity_translation_enabled('node')) { - // Make sure that every translation of a node has an alias. - $query->leftJoin('entity_translation', 't', "t.entity_type = 'node' AND t.entity_id = n.nid"); - $query->leftJoin('url_alias', 'ua', "CONCAT('node/', n.nid) AND ua.`language` = t.`language`"); - $query->leftJoin('url_alias', 'ua', "CONCAT('node/', n.nid) = ua.source"); - $query->addField('t', 'language'); - } - else { - $query->leftJoin('url_alias', 'ua', "CONCAT('node/', n.nid) = ua.source"); - $query->addField('n', 'language'); - } - - $query->addField('n', 'nid'); - $query->isNull('ua.source'); - $query->condition('n.nid', $context['sandbox']['current'], '>'); - $query->orderBy('n.nid'); - $query->addTag('pathauto_bulk_update'); - $query->addMetaData('entity', 'node'); - - // Get the total amount of items to process. - if (!isset($context['sandbox']['total'])) { - $context['sandbox']['total'] = $query->countQuery()->execute()->fetchField(); - - // If there are no nodes to update, the stop immediately. - if (!$context['sandbox']['total']) { - $context['finished'] = 1; - return; - } - } - - $query->range(0, 25); - $items = $query->execute()->fetchAll(); - - // Group by language. - $language_nids = array(); - foreach ($items as $item) { - $language_nids[$item->language][] = $item->nid; - } - - foreach ($language_nids as $language => $nids) { - pathauto_node_update_alias_multiple($nids, 'bulkupdate', array('language' => $language)); - $context['sandbox']['count'] += count($nids); - $context['sandbox']['current'] = max($nids); - } - $context['message'] = t('Updated alias for node @nid.', array('@nid' => end($nids))); - - if ($context['sandbox']['count'] != $context['sandbox']['total']) { - $context['finished'] = $context['sandbox']['count'] / $context['sandbox']['total']; + elseif (variable_get('entity_translation_languages_enabled', FALSE)) { + $languages = language_list('enabled'); + return $languages[1]; } + return language_list(); } diff --git a/entity_translation.node-form.js b/entity_translation.node-form.js index 5c8561f..f407999 100644 --- a/entity_translation.node-form.js +++ b/entity_translation.node-form.js @@ -4,9 +4,15 @@ Drupal.behaviors.translationNodeFieldsetSummaries = { attach: function (context) { $('fieldset#edit-translation', context).drupalSetSummary(function (context) { - return $('#edit-translation-retranslate', context).is(':checked') ? - Drupal.t('Flag translations as outdated') : - Drupal.t('Don\'t flag translations as outdated'); + var status = $('#edit-translation-status', context).is(':checked') ? Drupal.t('Translation published') : Drupal.t('Translation not published'); + var translate; + if ($('#edit-translation-retranslate', context).size()) { + translate = $('#edit-translation-retranslate', context).is(':checked') ? Drupal.t('Flag translations as outdated') : Drupal.t('Do not flag translations as outdated'); + } + else { + translate = $('#edit-translation-translate', context).is(':checked') ? Drupal.t('Needs to be updated') : Drupal.t('Does not need to be updated'); + } + return status + ', ' + translate; }); } }; diff --git a/entity_translation.node.inc b/entity_translation.node.inc index 5cf88a7..5c32beb 100644 --- a/entity_translation.node.inc +++ b/entity_translation.node.inc @@ -11,22 +11,22 @@ define('ENTITY_TRANSLATION_ENABLED', 4); /** - * Do not show translation metadata. + * Hides translation metadata. */ define('ENTITY_TRANSLATION_METADATA_HIDE', 0); /** - * Add translation metadata to the original authoring information. + * Adds translation metadata to the original authoring information. */ define('ENTITY_TRANSLATION_METADATA_SHOW', 1); /** - * Replace the original authoring information with translation metadata. + * Replaces the original authoring information with translation metadata. */ define('ENTITY_TRANSLATION_METADATA_REPLACE', 2); /** - * Check if the given entity has node translation enabled. + * Checks if the given entity has node translation enabled. */ function entity_translation_node($entity_type, $node) { return $entity_type == 'node' && function_exists('translation_supported_type') && translation_supported_type($node->type); @@ -86,46 +86,14 @@ function entity_translation_node_supported_type($type) { } /** - * Perform alterations on the node edit form. - * - * Clean up the language selector to avoid the possibility to change the node - * language to a value already assigned to an existing translation. - * Convert the translation update status fieldset into a vartical tab. - */ -function entity_translation_node_alter_form(&$form, $form_state, $handler) { - $translations = $handler->getTranslations(); - - // Disable languages for existing translations, so it is not possible to - // switch this node to some language which is already in the translation set. - foreach ($translations->data as $langcode => $translation) { - if ($langcode != $translations->original) { - unset($form['language']['#options'][$langcode]); - } - } - if (count($translations->data) > 1) { - unset($form['language']['#options']['']); - } - - if (isset($form['translation'])) { - $form['translation'] += array( - '#group' => 'additional_settings', - '#weight' => 100, - '#attached' => array( - 'js' => array(drupal_get_path('module', 'entity_translation') . '/entity_translation.node-form.js'), - ), - ); - } -} - -/** * Implements hook_node_view(). * - * Provide content language switcher links to navigate among node translations. + * Provides content language switcher links to navigate among node translations. */ function entity_translation_node_view($node, $build_mode) { if (!empty($node->translations) && drupal_multilingual() && entity_translation_node_supported_type($node->type)) { $path = 'node/' . $node->nid; - $links = language_negotiation_get_switch_links(LANGUAGE_TYPE_CONTENT, $path); + $links = EntityTranslationDefaultHandler::languageSwitchLinks($path); if (is_object($links) && !empty($links->links)) { $handler = entity_translation_get_handler('node', $node); @@ -153,7 +121,7 @@ function entity_translation_node_view($node, $build_mode) { /** * Implements hook_form_FORM_ID_alter(). * - * Provide settings into the node content type form to choose for entity + * Provides settings into the node content type form to choose for entity * translation metadata and comment filtering. */ function entity_translation_form_node_type_form_alter(&$form, &$form_state) { @@ -183,7 +151,7 @@ function entity_translation_form_node_type_form_alter(&$form, &$form_state) { /** * Implements hook_preprocess_node(). * - * Alter node template variables to show/replace entity translation metadata. + * Alters node template variables to show/replace entity translation metadata. */ function entity_translation_preprocess_node(&$variables) { $node = $variables['node']; @@ -229,7 +197,7 @@ function entity_translation_preprocess_node(&$variables) { /** * Implements hook_query_TAG_alter(). * - * Filter out node comments by content language. + * Filters out node comments by content language. * * @todo Find a way to track node comment statistics per language. */ diff --git a/entity_translation_upgrade/entity_translation_upgrade.admin.inc b/entity_translation_upgrade/entity_translation_upgrade.admin.inc index 00d9586..2d8157d 100644 --- a/entity_translation_upgrade/entity_translation_upgrade.admin.inc +++ b/entity_translation_upgrade/entity_translation_upgrade.admin.inc @@ -2,7 +2,7 @@ /** * @file - * Convert node translations into field-based translations. + * Converts node translations into field-based translations. */ /** @@ -11,7 +11,7 @@ define('ENTITY_TRANSLATION_UPGRADE_BATCH_SIZE', 10); /** - * Start the batch process to perform the upgrade. + * Starts the batch process to perform the upgrade. */ function entity_translation_upgrade_start() { $batch = array( @@ -184,7 +184,7 @@ function entity_translation_upgrade_do(&$context) { } /** - * Remove the translation sets for all the upgraded nodes. + * Removes the translation sets for all the upgraded nodes. */ function entity_translation_upgrade_complete(&$context) { db_query('UPDATE {node} SET tnid = 0 WHERE nid IN (SELECT DISTINCT etuh.tnid FROM {entity_translation_upgrade_history} etuh)'); diff --git a/entity_translation_upgrade/entity_translation_upgrade.module b/entity_translation_upgrade/entity_translation_upgrade.module index 02530a1..e103ed2 100644 --- a/entity_translation_upgrade/entity_translation_upgrade.module +++ b/entity_translation_upgrade/entity_translation_upgrade.module @@ -2,7 +2,7 @@ /** * @file - * Provide permanent redirects for unavailable node translations. + * Provides permanent redirects for unavailable node translations. */ /** @@ -34,7 +34,7 @@ function entity_translation_upgrade_menu_alter(&$items) { /** * Access callback. * - * Perform a redirect to the corresponding field-based translation if the + * Performs a redirect to the corresponding field-based translation if the * current user has not the permission to access the requested node translation. */ function entity_translation_upgrade_access($node) { @@ -92,7 +92,7 @@ function entity_translation_upgrade_submit($form, &$form_state) { } /** - * Perform the redirect to original node with the given language. + * Performs the redirect to original node with the given language. */ function entity_translation_upgrade_redirect($nid, $langcode) { $languages = language_list(); @@ -100,7 +100,7 @@ function entity_translation_upgrade_redirect($nid, $langcode) { } /** - * Check that the requested path might belong to an upgraded translation. + * Checks wether the requested path belongs to an upgraded translation. */ function entity_translation_upgrade_check_path() { $result = arg(0) == 'node' && ($nid = arg(1)) && is_int($nid) && !node_load($nid); @@ -108,7 +108,7 @@ function entity_translation_upgrade_check_path() { } /** - * Load the upgrade history entry for the given nid. + * Loads the upgrade history entry for the given nid. */ function entity_translation_upgrade_load($nid) { return db_select('entity_translation_upgrade_history', 'etu') diff --git a/includes/translation.handler.comment.inc b/includes/translation.handler.comment.inc new file mode 100644 index 0000000..00342e0 --- /dev/null +++ b/includes/translation.handler.comment.inc @@ -0,0 +1,47 @@ +entity->language; + } + + /** + * @see EntityTranslationDefaultHandler::entityForm() + */ + public function entityForm(&$form, &$form_state) { + parent::entityForm($form, $form_state); + // Adjust the translation fieldset weight to move it below the admin one. + $form['translation']['#weight'] = 1; + } + + /** + * @see EntityTranslationDefaultHandler::entityFormTitle() + */ + protected function entityFormTitle() { + return t('Edit comment @subject', array('@subject' => $this->getLabel())); + } + + /** + * @see EntityTranslationDefaultHandler::getStatus() + */ + protected function getStatus() { + return (boolean) $this->entity->status; + } +} diff --git a/includes/translation.handler.inc b/includes/translation.handler.inc index ad081fa..2781322 100644 --- a/includes/translation.handler.inc +++ b/includes/translation.handler.inc @@ -15,32 +15,32 @@ interface EntityTranslationHandlerInterface { /** - * Load the translation data into the wrapped entity. + * Loads the translation data into the wrapped entity. */ public function loadTranslations(); /** - * Write the translation status to the storage. + * Writes the translation status to the storage. */ public function saveTranslations(); /** - * Return the translation data for the current (wrapped) entity. + * Returns the translation data for the current (wrapped) entity. */ public function getTranslations(); /** - * Add/edit an entity translation. + * Adds/updates an entity translation. * * @param $translation * A translation array as defined by the translation table's schema. * @param $values - * Optional. the values that should be assigned to the field translations. + * (optional) the values that should be assigned to the field translations. */ public function setTranslation($translation, $values = NULL); /** - * Remove a translation from the translation set. + * Removes a translation from the translation set. * * @param $langcode * The language code of the translation to be removed. @@ -48,17 +48,17 @@ interface EntityTranslationHandlerInterface { public function removeTranslation($langcode); /** - * Initialize the translation set by creating the original translation. + * Initializes the translation set by creating the original translation. */ public function initTranslations(); /** - * Update the translation set from the current entity status. + * Updates the translation set from the current entity status. */ public function updateTranslations(); /** - * Remove all translations from the translation set. + * Removes all translations from the translation set. */ public function removeTranslations(); @@ -74,12 +74,12 @@ interface EntityTranslationHandlerInterface { public function initOriginalTranslation(); /** - * Return the entity language. + * Returns the entity language. */ public function getLanguage(); /** - * Set the language of the orginal translation. + * Sets the language of the orginal translation. * * @param $langcode * The language code of the original content values. @@ -87,12 +87,12 @@ interface EntityTranslationHandlerInterface { public function setOriginalLanguage($langcode); /** - * Return TRUE if the entity is currently being translated. + * Returns TRUE if the entity is currently being translated. */ public function isTranslating(); /** - * Notify the translation handler that its wrapped entity is being translated. + * Notifies the translation handler that its entity is being translated. * * @param $translating * A boolean value. @@ -105,7 +105,7 @@ interface EntityTranslationHandlerInterface { public function isRevision(); /** - * Replace the wrapped entity. + * Replaces the wrapped entity. * * @param $entity * The entity to be translated. @@ -113,7 +113,7 @@ interface EntityTranslationHandlerInterface { public function setEntity($entity); /** - * Set the translation update status. + * Sets the translation update status. * * @param $outdated * A boolean value. @@ -121,7 +121,7 @@ interface EntityTranslationHandlerInterface { public function setOutdated($outdated); /** - * Return the base path for the current entity. + * Returns the base path for the current entity. * * This path will be prepended to the URL of any administration page. * @@ -131,12 +131,15 @@ interface EntityTranslationHandlerInterface { public function getBasePath(); /** - * Return the path of the entity edit form. + * Returns the path of the entity edit form. + * + * @param $langcode + * (optional) The language the edit form should be presented in. */ - public function getEditPath(); + public function getEditPath($langcode = NULL); /** - * Return the path of the entity view page. + * Returns the path of the entity view page. */ public function getViewPath(); @@ -146,7 +149,7 @@ interface EntityTranslationHandlerInterface { public function getLabel(); /** - * Check if the user can perform the given operation on the wrapped entity. + * Checks if the user can perform the given operation on the wrapped entity. * * @param $op * The operation to be performed. @@ -158,9 +161,59 @@ interface EntityTranslationHandlerInterface { public function getAccess($op); /** + * Checks if a user is allowed to edit the given translation. + */ + public function getTranslationAccess($langcode); + + /** * Return TRUE if the entity supports URL aliasing. */ public function isAliasEnabled(); + + /** + * Sets the active form language. + */ + public function setFormLanguage($langcode); + + /** + * Retrieves the active form language. + */ + public function getFormLanguage(); + + /** + * Sets the source language for the translation being created. + */ + public function setSourceLanguage($langcode); + + /** + * Retrieves the source language for the translation being created. + */ + public function getSourceLanguage(); + + /** + * Returns TRUE if a new entity is currently wrapped. + */ + public function isNewEntity(); + + /** + * Performs the needed alterations to the entity form. + */ + public function entityForm(&$form, &$form_state); + + /** + * Performs validation tasks on the submitted entity forms. + */ + public function entityFormValidate($form, &$form_state); + + /** + * Performs submission tasks on the submitted entity forms. + */ + public function entityFormSubmit($form, &$form_state); + + /** + * Alters the local tasks render array to populate the language tabs. + */ + public function localTasksAlter(&$data, $router_item, $root_path); } /** @@ -175,11 +228,25 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa private $translating; private $outdated; + private $formLanguage; + private $sourceLanguage; private $basePath; private $editPath; private $viewPath; + /** + * Initializes an instance of the translation handler. + * + * @param $entity_type + * The type of the entity being wrapped. + * @param $entity_info + * The entity information for the entity being wrapped. + * @param $entity + * The entity being wrapped. + * @param $entity_id + * The identifier of the entity being wrapped. + */ public function __construct($entity_type, $entity_info, $entity, $entity_id) { $this->entityType = $entity_type; $this->entityInfo = $entity_info; @@ -188,6 +255,8 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa $this->translating = FALSE; $this->outdated = FALSE; + $this->formLanguage = FALSE; + $this->sourceLanguage = FALSE; $info = $entity_info['translation']['entity_translation']; $this->basePath = $this->getPathInstance($info['base path']); @@ -225,6 +294,23 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa } } + /** + * Returns the localized links for the given path. + */ + public static function languageSwitchLinks($path) { + $links = language_negotiation_get_switch_links(LANGUAGE_TYPE_CONTENT, $path); + if (empty($links)) { + // If content language is set up to fall back to the interface language, + // then there will be no switch links for LANGUAGE_TYPE_CONTENT, ergo we + // also need to use interface switch links. + $links = language_negotiation_get_switch_links(LANGUAGE_TYPE_INTERFACE, $path); + } + return $links; + } + + /** + * @see EntityTranslationHandlerInterface::loadTranslations() + */ public function loadTranslations() { if (isset($this->entityId)) { $this->loadMultiple($this->entityType, array($this->entityId => $this->entity)); @@ -234,6 +320,9 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa } } + /** + * @see EntityTranslationHandlerInterface::saveTranslations() + */ public function saveTranslations() { // Delete and insert, rather than update, in case a value was added. db_delete('entity_translation') @@ -268,9 +357,6 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa foreach ($translations->data as $langcode => $translation) { $translation = $overrides + $translation + $defaults; - if ($this->outdated && $langcode != $translations->original) { - $translation['translate'] = 1; - } $query->values($translation); } @@ -278,6 +364,9 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa } } + /** + * @see EntityTranslationHandlerInterface::getTranslations() + */ public function getTranslations() { $translations_key = $this->getTranslationsKey(); @@ -290,6 +379,9 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa return $this->entity->{$translations_key}; } + /** + * @see EntityTranslationHandlerInterface::setTranslation() + */ public function setTranslation($translation, $values = NULL) { if (isset($translation['source']) && $translation['language'] == $translation['source']) { throw new Exception('Invalid translation language'); @@ -320,6 +412,9 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa } } + /** + * @see EntityTranslationHandlerInterface::removeTranslation() + */ public function removeTranslation($langcode) { $translations_key = $this->getTranslationsKey(); @@ -348,6 +443,9 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa } } + /** + * @see EntityTranslationHandlerInterface::initTranslations() + */ public function initTranslations() { $langcode = $this->getLanguage(); @@ -358,6 +456,9 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa } } + /** + * @see EntityTranslationHandlerInterface::updateTranslations() + */ public function updateTranslations() { $langcode = $this->getLanguage(); @@ -372,10 +473,16 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa } } + /** + * @see EntityTranslationHandlerInterface::removeTranslations() + */ public function removeTranslations() { $this->removeTranslation(NULL); } + /** + * @see EntityTranslationHandlerInterface::initOriginalTranslation() + */ public function initOriginalTranslation() { $fixed = FALSE; $translations = $this->getTranslations(); @@ -396,14 +503,22 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa return $fixed; } + /** + * @see EntityTranslationHandlerInterface::getLanguage() + */ public function getLanguage() { + if (!empty($this->entityInfo['entity keys']['language'])) { + $language_key = $this->entityInfo['entity keys']['language']; + if (!empty($this->entity->{$language_key})) { + return $this->entity->{$language_key}; + } + } return language_default()->language; } - public function setLanguage($langcode) { - $this->language = $langcode; - } - + /** + * @see EntityTranslationHandlerInterface::setOriginalLanguage() + */ public function setOriginalLanguage($langcode) { $translations = $this->getTranslations(); @@ -416,38 +531,72 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa $translations->original = $langcode; } + /** + * @see EntityTranslationHandlerInterface::isTranslating() + */ public function isTranslating() { return $this->translating; } + /** + * @see EntityTranslationHandlerInterface::setTranslating() + */ public function setTranslating($translating) { $this->translating = $translating; } + /** + * @see EntityTranslationHandlerInterface::isRevision() + */ public function isRevision() { return FALSE; } + /** + * @see EntityTranslationHandlerInterface::setEntity() + */ public function setEntity($entity) { $this->entity = $entity; } + /** + * @see EntityTranslationHandlerInterface::setOutdated() + */ public function setOutdated($outdated) { - $this->outdated = $outdated; + if ($outdated) { + $translations = $this->getTranslations(); + foreach ($translations->data as $langcode => &$translation) { + if ($langcode != $this->getFormLanguage()) { + $translation['translate'] = 1; + } + } + } } + /** + * @see EntityTranslationHandlerInterface::getBasePath() + */ public function getBasePath() { return $this->basePath; } - public function getEditPath() { - return $this->editPath; + /** + * @see EntityTranslationHandlerInterface::getEditPath() + */ + public function getEditPath($langcode = NULL) { + return empty($langcode) ? $this->editPath : $this->editPath . '/' . $langcode; } + /** + * @see EntityTranslationHandlerInterface::getViewPath() + */ public function getViewPath() { return $this->viewPath; } + /** + * @see EntityTranslationHandlerInterface::getLabel() + */ public function getLabel() { if (($label = entity_label($this->entityType, $this->entity)) !== FALSE) { return $label; @@ -457,37 +606,410 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa } } + /** + * @see EntityTranslationHandlerInterface::getAccess() + */ public function getAccess($op) { return TRUE; } + /** + * @see EntityTranslationHandlerInterface::getTranslationAccess() + */ + public function getTranslationAccess($langcode) { + return $langcode != $this->getLanguage() || user_access('edit original values'); + } + + /** + * @see EntityTranslationHandlerInterface::isAliasEnabled() + */ public function isAliasEnabled() { return !empty($this->entityInfo['translation']['entity_translation']['alias']); } /** - * Return the translation object key for the wrapped entity type. + * @see EntityTranslationHandlerInterface::setFormLanguage() + */ + public function setFormLanguage($langcode) { + $this->formLanguage = $langcode; + } + + /** + * @see EntityTranslationHandlerInterface::getFormLanguage() + */ + public function getFormLanguage() { + return $this->formLanguage; + } + + /** + * @see EntityTranslationHandlerInterface::setSourceLanguage() + */ + public function setSourceLanguage($langcode) { + $this->sourceLanguage = $langcode; + } + + /** + * @see EntityTranslationHandlerInterface::getSourceLanguage() + */ + public function getSourceLanguage() { + return $this->sourceLanguage; + } + + /** + * @see EntityTranslationHandlerInterface::isNewEntity() + */ + public function isNewEntity() { + $id = $this->getEntityId(); + return empty($id); + } + + /** + * @see EntityTranslationHandlerInterface::entityForm() + */ + public function entityForm(&$form, &$form_state) { + $translations = $this->getTranslations(); + $form_langcode = $this->getFormLanguage(); + $langcode = $this->getLanguage(); + $is_translation = $this->isTranslationForm(); + $new_translation = !isset($translations->data[$form_langcode]); + $source_language = $this->getSourceLanguage(); + $no_translations = count($translations->data) < 2; + $languages = language_list(); + + // Adjust page title to specify the current language being edited, if + // we have at least one translation. + if (!$no_translations || $new_translation) { + drupal_set_title($this->entityFormTitle() . ' [' . t($languages[$form_langcode]->name) . ']', PASS_THROUGH); + } + + // Display source language selector only if we are creating a new + // translation and there are at least two translations available. + if (!$no_translations && $new_translation) { + $form['source_language'] = array( + '#type' => 'fieldset', + '#title' => t('Source language'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#tree' => TRUE, + '#weight' => -100, + 'language' => array( + '#type' => 'select', + '#default_value' => $this->getSourceLanguage(), + '#options' => array(), + ), + 'submit' => array( + '#type' => 'submit', + '#value' => t('Change'), + '#submit' => array('entity_translation_entity_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); + } + } + } + + // Disable languages for existing translations, so it is not possible to + // switch this node to some language which is already in the translation + // set. + $language_widget = isset($form['language']) && $form['language']['#type'] == 'select'; + if ($language_widget) { + foreach ($translations->data as $translation) { + if (!empty($translation['source'])) { + unset($form['language']['#options'][$translation['language']]); + } + } + if (count($translations->data) > 1) { + unset($form['language']['#options'][LANGUAGE_NONE]); + } + } + + if ($is_translation) { + // If we are editing a translation set the correct value in the language + // widget and not current one. + // @todo Consider supporting the ability to change translation language. + if ($language_widget) { + $form['language']['#options'][$langcode] = $languages[$langcode]->name; + $form['language']['#default_value'] = $langcode; + $form['language']['#disabled'] = TRUE; + } + + // Replace the delete button with the delete translation one. + if (!$new_translation) { + $weight = 100; + foreach (array('delete', 'submit') as $key) { + if (isset($form['actions'][$key]['weight'])) { + $weight = $form['actions'][$key]['weight']; + break; + } + } + $form['actions']['delete_translation'] = array( + '#type' => 'submit', + '#value' => t('Delete translation'), + '#weight' => $weight, + '#submit' => array('entity_translation_entity_form_delete_translation_submit'), + ); + } + + // Always remove the delete button on translation forms. + unset($form['actions']['delete']); + } + + // We need to display the translation tab only when there is at least one + // translation available or a new one is about to be created. + if ($new_translation || count($translations->data) > 1) { + $form['translation'] = array( + '#type' => 'fieldset', + '#title' => t('Translation'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#tree' => TRUE, + '#weight' => 10, + '#access' => user_access('translate any entity') || user_access("translate $this->entityType entities"), + ); + + $status = $new_translation || $translations->data[$form_langcode]['status']; + $enabled = !$status; + // If there is only one published translation we cannot unpublish it, + // since there would be no content left to display. The whole entity + // should be unpublished instead, where possible. + if (!empty($status)) { + $published = 0; + foreach ($translations->data as $langcode => $translation) { + $published += $translation['status']; + } + $enabled = $published > 1; + } + $description = $enabled ? + t('An unpublished translation will not be visible for non-administrators.') : + t('Only this translation is published. You must publish at least one more translation to unpublish this one.'); + + $form['translation']['status'] = array( + '#type' => 'checkbox', + '#title' => t('This translation is published'), + '#default_value' => $status, + '#description' => $description, + '#disabled' => !$enabled, + ); + + $translate = !$new_translation && $translations->data[$form_langcode]['translate']; + if (!$translate) { + $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.'), + ); + } + else { + $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, + ); + } + + $name = $new_translation ? $GLOBALS['user']->name : user_load($translations->data[$form_langcode]['uid'])->name; + $form['translation']['name'] = array( + '#type' => 'textfield', + '#title' => t('Authored by'), + '#maxlength' => 60, + '#autocomplete_path' => 'user/autocomplete', + '#default_value' => $name, + '#description' => t('Leave blank for %anonymous.', array('%anonymous' => variable_get('anonymous', t('Anonymous')))), + ); + + $date = $new_translation ? REQUEST_TIME : $translations->data[$form_langcode]['created']; + $form['translation']['created'] = array( + '#type' => 'textfield', + '#title' => t('Authored on'), + '#maxlength' => 25, + '#description' => t('Format: %time. The date format is YYYY-MM-DD and %timezone is the time zone offset from UTC. Leave blank to use the time of form submission.', array('%time' => format_date($date, 'custom', 'Y-m-d H:i:s O'), '%timezone' => format_date($date, 'custom', 'O'))), + '#default_value' => $new_translation ? '' : format_date($date, 'custom', 'Y-m-d H:i:s O'), + ); + } + + // We need to process the posted form as early as possible to update the + // form language value. + array_unshift($form['#validate'], 'entity_translation_entity_form_validate'); + + // Process entity form submission. + $form['#submit'][] = 'entity_translation_entity_form_submit'; + + // This allows to intercept deletions. + $form['actions']['delete']['#submit'][] = 'entity_translation_entity_form_submit'; + } + + /** + * @see EntityTranslationHandlerInterface::entityFormValidate() + */ + public function entityFormValidate($form, &$form_state) { + // Update the form language as it might have changed. We exploit the + // validation phase to be sure to act as early as possible. + if (isset($form_state['values']['language']) && !$this->isTranslationForm()) { + $this->setFormLanguage($form_state['values']['language']); + } + } + + /** + * @see EntityTranslationHandlerInterface::entityFormSubmit() + */ + public function entityFormSubmit($form, &$form_state) { + $langcode = $this->getLanguage(); + $form_langcode = $this->getFormLanguage(); + $translations = $this->getTranslations(); + $is_translation = $this->isTranslationForm(); + $new_translation = !isset($translations->data[$form_langcode]); + $values = isset($form_state['values']['translation']) ? $form_state['values']['translation'] : array(); + + // Ensure every key has at least a default value. Subclasses may provide use + // entity-specific values to alter them. + $values += array( + 'status' => TRUE, + 'retranslate' => 0, + 'name' => $GLOBALS['user']->name, + ); + + if (!isset($translations->data[$form_langcode])) { + // If we have a new translation the language is the original entity + // language. + $translation = $is_translation ? array('language' => $form_langcode, 'source' => $this->getSourceLanguage()) : array('language' => $langcode, 'source' => ''); + } + else { + $translation = $translations->data[$form_langcode]; + } + + if (isset($values['translate'])) { + $translation['translate'] = intval($values['translate']); + } + else { + $this->setOutdated($values['retranslate']); + } + + // Handle possible language changes for the original values. + if (!$is_translation) { + $this->setOriginalLanguage($langcode); + } + + $translation['status'] = intval($values['status']); + $translation['uid'] = user_load_by_name($values['name'])->uid; + $translation['created'] = empty($values['created']) ? REQUEST_TIME : strtotime($values['created']); + $this->setTranslation($translation); + + // If no redirect has been explicitly set, go to the edit form for the + // current form language. + if ($new_translation && empty($form_state['redirect']) && !$this->isNewEntity()) { + $form_state['redirect'] = $this->getEditPath($form_langcode); + } + } + + /** + * @see EntityTranslationHandlerInterface::localTasksAlter() + */ + public function localTasksAlter(&$data, $router_item, $root_path) { + $translations = $this->getTranslations(); + + if (count($translations->data) > 0) { + $languages = language_list(); + $form_langcode = $this->getFormLanguage(); + $language_tabs = array(); + + if ($this->getSourceLanguage()) { + foreach ($data['tabs'][1]['output'] as $index => &$add_tab) { + if ($add_tab['#link']['path'] == $root_path) { + $add_tab['#link']['title'] = $languages[$form_langcode]->name; + $add_tab['#link']['weight'] = $languages[$form_langcode]->weight; + $add_tab['#active'] = TRUE; + $add_tab['#language_tab'] = TRUE; + $language_tabs[] = $add_tab; + unset($data['tabs'][1]['output'][$index]); + break; + } + } + } + + foreach ($translations->data as $langcode => $translation) { + if ($this->getTranslationAccess($langcode)) { + $links = $this->languageSwitchLinks($this->getEditPath($langcode)); + $link = $links->links[$langcode]; + + if (isset($link['href'])) { + $tab = array(); + $tab['#theme'] = 'menu_local_task'; + $tab['#active'] = $langcode == $form_langcode; + $tab['#language_tab'] = TRUE; + $tab['#link'] = array( + 'href' => $link['href'], + 'title' => t($languages[$langcode]->name), + 'weight' => $languages[$langcode]->weight, + 'localized_options' => $link, + ) + $router_item; + $language_tabs[] = $tab; + } + } + } + + // Reorder tabs to make the add tab respect language weights. + usort($language_tabs, array($this, 'translationTabSort')); + + // Merge the reordered language tabs into the second level tabs. + if (count($language_tabs) > 1) { + if (empty($data['tabs'][1])) { + $data['tabs'][1] = array('output' => array()); + } + $data['tabs'][1]['output'] = array_merge($data['tabs'][1]['output'], $language_tabs); + $data['tabs'][1]['count'] = count($data['tabs'][1]['output']); + } + } + } + + /** + * Helper callback. Sorts language tabs by weight. + */ + protected function translationTabSort($a, $b) { + return $a['#link']['weight'] > $b['#link']['weight']; + } + + /** + * Returns the title to be used for the entity form page. + */ + protected function entityFormTitle() { + return $this->getLabel(); + } + + /** + * Returns TRUE if an entity translation is being edited. + */ + protected function isTranslationForm() { + return !$this->isNewEntity() && $this->getFormLanguage() != $this->getLanguage(); + } + + /** + * Returns the translation object key for the wrapped entity type. */ protected function getTranslationsKey() { return $this->entityInfo['entity keys']['translations']; } /** - * Return the entity accessibility. + * Returns the entity accessibility. */ protected function getStatus() { return TRUE; } /** - * Return the entity identifier. + * Returns the entity identifier. */ protected function getEntityId() { return $this->entityId; } /** - * Return an instance of the given path. + * Returns an instance of the given path. * * @param $path * An internal path containing the entity id wildcard. @@ -501,7 +1023,7 @@ class EntityTranslationDefaultHandler implements EntityTranslationHandlerInterfa } /** - * Return an empty translations data structure. + * Returns an empty translations data structure. */ protected static function emptyTranslations() { return (object) array('original' => NULL, 'data' => array()); diff --git a/includes/translation.handler.node.inc b/includes/translation.handler.node.inc index 00e17cf..de91896 100644 --- a/includes/translation.handler.node.inc +++ b/includes/translation.handler.node.inc @@ -2,7 +2,7 @@ /** * @file - * Node translation handler for the translation module. + * Node translation handler for the entity translation module. */ @@ -17,18 +17,74 @@ class EntityTranslationNodeHandler extends EntityTranslationDefaultHandler { parent::__construct('node', $entity_info, $entity, $entity_id); } + /** + * @see EntityTranslationDefaultHandler::isRevision() + */ public function isRevision() { return !empty($this->entity->revision); } - public function getLanguage() { - return $this->entity->language; - } - + /** + * @see EntityTranslationDefaultHandler::getAccess() + */ public function getAccess($op) { return node_access($op, $this->entity); } + /** + * Convert the translation update status fieldset into a vartical tab. + */ + public function entityForm(&$form, &$form_state) { + parent::entityForm($form, $form_state); + + // Move the translation fieldset to a vertical tab. + if (isset($form['translation'])) { + $form['translation'] += array( + '#group' => 'additional_settings', + '#weight' => 100, + '#attached' => array( + 'js' => array(drupal_get_path('module', 'entity_translation') . '/entity_translation.node-form.js'), + ), + ); + + if (!$this->isTranslationForm()) { + $form['translation']['name']['#access'] = FALSE; + $form['translation']['created']['#access'] = FALSE; + } + } + } + + /** + * @see EntityTranslationDefaultHandler::entityFormSubmit() + */ + public function entityFormSubmit($form, &$form_state) { + if (!isset($form_state['values']['translation'])) { + $form_state['values']['translation'] = array('status' => $form_state['values']['status']); + } + $values = &$form_state['values']['translation']; + + if (!$this->isTranslationForm()) { + // Inherit entity authoring information for the original values. + $values['name'] = $form_state['values']['name']; + if (!empty($form_state['values']['date'])) { + $values['created'] = $form_state['values']['date']; + } + } + + parent::entityFormSubmit($form, $form_state); + } + + /** + * @see EntityTranslationDefaultHandler::entityFormTitle() + */ + protected function entityFormTitle() { + $type_name = node_type_get_name($this->entity); + return t('Edit @type @title', array('@type' => $type_name, '@title' => $this->getLabel())); + } + + /** + * @see EntityTranslationDefaultHandler::getStatus() + */ protected function getStatus() { return (boolean) $this->entity->status; } diff --git a/includes/translation.handler.taxonomy_term.inc b/includes/translation.handler.taxonomy_term.inc new file mode 100644 index 0000000..7bcec00 --- /dev/null +++ b/includes/translation.handler.taxonomy_term.inc @@ -0,0 +1,30 @@ +drupalGet('node/' . $node->nid . '/translate/add/es/en'); + $this->drupalGet('node/' . $node->nid . '/edit/add/en/es'); $body_key = "body[$langcode][0][value]"; $this->assertFieldByXPath("//textarea[@name='$body_key']", $node->body[$node->language][0]['value'], 'Original body value correctly populated.'); @@ -198,8 +198,9 @@ class EntityTranslationTestCase extends DrupalWebTestCase { $edit = array(); $edit[$body_key] = $body; - $this->drupalPost(NULL, $edit, t('Save translation')); - $this->assertLinkByHref('node/' . $node->nid . '/translate/edit/' . $langcode, 0, t('Translation edit link found. Translation created.')); + $this->drupalPost(NULL, $edit, t('Save')); + $this->drupalGet('node/' . $node->nid . '/translate'); + $this->assertLinkByHref('node/' . $node->nid . '/edit/' . $langcode, 0, t('Translation edit link found. Translation created.')); return $node; }