diff --git a/entity_translation.admin.inc b/entity_translation.admin.inc index a341452..5bfac53 100644 --- a/entity_translation.admin.inc +++ b/entity_translation.admin.inc @@ -520,3 +520,195 @@ function entity_translation_delete_confirm_submit($form, &$form_state) { $form_state['redirect'] = "{$handler->getBasePath()}/translate"; } + +/* + * Confirm form for changing field translatability. + */ +function entity_translation_translatable_form($form, &$form_state, $field_name) { + $field = field_info_field($field_name); + $t_args = array('%name' => $field_name); + + $warning = t('By submitting this form you will trigger a batch operation.'); + if ($field['translatable']) { + $title = t('Are you sure you want to disable translation for the %name field?', $t_args); + $warning .= "
" . t("All the existing translations of this field will be deleted.
This action cannot be undone."); + } + else { + $title = t('Are you sure you want to enable translation for the %name field?', $t_args); + } + + // We need to keep some information for later processing. + $form_state['field'] = $field; + + return confirm_form($form, $title, '', $warning); +} + +/** + * Submit handler for the field settings form. + * + * This submit handler maintains consistency between the translatability of an + * entity and the language under which the field data is stored. When a field is + * marked as translatable, all the data in $entity->{field_name}[LANGUAGE_NONE] + * is moved to $entity->{field_name}[$entity_language]. When a field is marked + * as untranslatable the opposite process occurs. Note that marking a field as + * untranslatable will cause all of its translations to be permanently removed, + * with the exception of the one corresponding to the entity language. + */ +function entity_translation_translatable_form_submit($form, $form_state) { + // This is the current state that we want to reverse. + $translatable = $form_state['field']['translatable']; + $field_name = $form_state['field']['field_name']; + $field = field_info_field($field_name); + + if ($field['translatable'] !== $translatable) { + // Field translatability has changed since form creation, abort. + return; + } + + // If a field is untranslatable, it can have no data except under + // LANGUAGE_NONE. Thus we need a field to be translatable before we convert + // data to the entity language. Conversely we need to switch data back to + // LANGUAGE_NONE before making a field untranslatable lest we lose + // information. + $operations = array( + array('entity_translation_translatable_batch', array(!$translatable, $field_name)), + array('entity_translation_translatable_switch', array(!$translatable, $field_name)), + ); + $operations = $translatable ? $operations : array_reverse($operations); + + $t_args = array('%field' => $field_name); + $title = !$translatable ? t('Enabling translation for the %field field .', $t_args) : t('Disabling translation for the %field field .', $t_args); + + $batch = array( + 'title' => $title, + 'operations' => $operations, + 'finished' => 'entity_translation_translatable_batch_done', + 'file' => drupal_get_path('module', 'entity_translation') . '/entity_translation.admin.inc', + ); + + batch_set($batch); +} + +/* + * Toggle translatability of the given field. + * + * This is called from a batch operation, but should only run once per field. + */ +function entity_translation_translatable_switch($translatable, $field_name) { + $field = field_info_field($field_name); + + if ($field['translatable'] === $translatable) { + return; + } + + $field['translatable'] = $translatable; + field_update_field($field); + + // This is needed for versions of Drupal core 7.10 and lower. + // See http://drupal.org/node/1380660 for details. + drupal_static_reset('field_available_languages'); +} + +/** + * Batch operation. Convert field data to or from LANGUAGE_NONE. + */ +function entity_translation_translatable_batch($translatable, $field_name, &$context) { + if (empty($context['sandbox'])) { + $context['sandbox']['progress'] = 0; + + // How many entities will need processing? + $query = new EntityFieldQuery(); + $count = $query + ->fieldCondition($field_name) + ->count() + ->execute(); + + if (intval($count) === 0) { + // Nothing to do. + $context['finished'] = 1; + return; + } + $context['sandbox']['max'] = $count; + } + + // Number of entities to be processed for each step. + $limit = variable_get('entity_translation_translatable_batch_limit', 10); + + $query = new EntityFieldQuery(); + $result = $query + ->fieldCondition($field_name) + ->range($context['sandbox']['progress'], $limit) + ->execute(); + + foreach ($result as $entity_type => $entities) { + foreach (entity_load($entity_type, array_keys($entities)) as $id => $entity) { + $context['sandbox']['progress']++; + $handler = entity_translation_get_handler($entity_type, $entity); + $langcode = $handler->getLanguage(); + + // We need a two-steps approach while updating field translations: given + // that field-specific update functions might rely on the stored values to + // perform their processing, see for instance file_field_update(), first + // we need to store the new translations and only after we can remove the + // old ones. Otherwise we might have data loss, since the removal of the + // old translations might occur before the new ones are stored. + if ($translatable && isset($entity->{$field_name}[LANGUAGE_NONE])) { + // If the field is being switched to translatable and has data for + // LANGUAGE_NONE then we need to move the data to the right language. + $entity->{$field_name}[$langcode] = $entity->{$field_name}[LANGUAGE_NONE]; + // Store the original value. + _entity_translation_update_field($entity_type, $entity, $field_name); + $entity->{$field_name}[LANGUAGE_NONE] = array(); + // Remove the language neutral value. + _entity_translation_update_field($entity_type, $entity, $field_name); + } + elseif (!$translatable && isset($entity->{$field_name}[$langcode])) { + // The field has been marked untranslatable and has data in the entity + // language: we need to move it to LANGUAGE_NONE and drop the other + // translations. + $entity->{$field_name}[LANGUAGE_NONE] = $entity->{$field_name}[$langcode]; + // Store the original value. + _entity_translation_update_field($entity_type, $entity, $field_name); + // Remove translations. + foreach ($entity->{$field_name} as $langcode => $items) { + if ($langcode != LANGUAGE_NONE) { + $entity->{$field_name}[$langcode] = array(); + } + } + _entity_translation_update_field($entity_type, $entity, $field_name); + } + else { + // No need to save unchanged entities. + continue; + } + } + } + + $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; +} + +/** + * Stores the given field translations. + */ +function _entity_translation_update_field($entity_type, $entity, $field_name) { + $field = field_info_field($field_name); + // Ensure that we are trying to store only valid data. + foreach ($entity->{$field_name} as $langcode => $items) { + $entity->{$field_name}[$langcode] = _field_filter_items($field, $entity->{$field_name}[$langcode]); + } + field_attach_presave($entity_type, $entity); + field_attach_update($entity_type, $entity); +} + +/** + * Check the exit status of the batch operation. + */ +function entity_translation_translatable_batch_done($success, $results, $operations) { + if ($success) { + drupal_set_message(t("Data successfully processed.")); + } + else { + // @todo: Do something about this case. + drupal_set_message(t("Something went wrong while processing data. Some nodes may appear to have lost fields.")); + } +} diff --git a/entity_translation.module b/entity_translation.module index 1f0b4bd..b70cc41 100644 --- a/entity_translation.module +++ b/entity_translation.module @@ -63,11 +63,17 @@ function entity_translation_entity_info_alter(&$entity_info) { // Provide defaults for translation info. foreach ($entity_info as $entity_type => $info) { - if (entity_translation_enabled($entity_type, TRUE)) { - if (!isset($entity_info[$entity_type]['translation']['entity_translation'])) { - $entity_info[$entity_type]['translation']['entity_translation'] = array(); - } + if (!isset($entity_info[$entity_type]['translation']['entity_translation'])) { + $entity_info[$entity_type]['translation']['entity_translation'] = array(); + } + // Every fieldable entity type must have a translation handler class, no + // matter if it is enabled for translation or not. As a matter of fact we + // might need it to correctly switch field translatability when a field is + // shared accross different entities. + $entity_info[$entity_type]['translation']['entity_translation'] += array('class' => 'EntityTranslationDefaultHandler'); + + if (entity_translation_enabled($entity_type, TRUE)) { // 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'])) { @@ -80,7 +86,6 @@ function entity_translation_entity_info_alter(&$entity_info) { // If we cannot find a usable base path we skip to the next entity type. if (!isset($router["$entity_type/%"])) { - unset($entity_info[$entity_type]['translation']['entity_translation']); continue; } @@ -93,7 +98,6 @@ function entity_translation_entity_info_alter(&$entity_info) { $entity_info[$entity_type]['translation']['entity_translation'] += array( 'view path' => $path, 'edit path' => "$path/edit", - 'class' => 'EntityTranslationDefaultHandler', 'path wildcard' => "%$entity_type", 'access callback' => 'entity_translation_tab_access', 'access arguments' => array($entity_type), @@ -120,6 +124,24 @@ function entity_translation_enabled($entity_type, $skip_handler = FALSE) { } /** + * Implments hook_menu(). + */ +function entity_translation_menu() { + $items = array(); + + $items['admin/config/regional/entity_translation/translatable/%'] = array( + 'title' => 'Confirm change in translatability.', + 'description' => 'Confirm page for changing field translatability.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('entity_translation_translatable_form', 5), + 'access arguments' => array('toggle field translatability'), + 'file' => 'entity_translation.admin.inc', + ); + + return $items; +} + +/** * Implements hook_menu_alter(). */ function entity_translation_menu_alter(&$items) { @@ -211,6 +233,7 @@ function entity_translation_menu_alter(&$items) { return $items; } + /** * Implements hook_admin_paths(). */ @@ -276,6 +299,10 @@ function entity_translation_permission() { 'title' => t('Administer entity translation'), 'description' => t('Select which entities can be translated.'), ), + 'toggle field translatability' => array( + 'title' => t('Toggle field translatability'), + 'description' => t('Toggle translatability of fields performing a bulk update.'), + ), ); foreach (entity_get_info() as $entity_type => $info) { if ($info['fieldable']) { @@ -480,14 +507,39 @@ function entity_translation_edit_form_submit($form, &$form_state) { * * Enable a selector to choose whether a field is translatable. */ -function entity_translation_form_field_ui_field_settings_form_alter(&$form, $form_state) { - $instance = $form_state['build_info']['args'][0]; - $field = field_info_field($instance['field_name']); - $form['field']['translatable'] = array( - '#type' => 'checkbox', - '#title' => t('Users may translate this field.'), - '#default_value' => $field['translatable'], - ); +function entity_translation_form_field_ui_field_edit_form_alter(&$form, $form_state) { + $field_name = $form['#field']['field_name']; + $field = field_info_field($field_name); + $translatable = $field['translatable']; + $title = t('Users may translate this field.'); + + if (field_has_data($field)) { + $path = "admin/config/regional/entity_translation/translatable/$field_name"; + $status = $translatable ? $title : t('This field is shared among the entity translations.'); + $link_title = !$translatable ? t('Enable translation') : t('Disable translation'); + + $form['field']['translatable'] = array( + '#prefix' => '
', + '#suffix' => '
', + 'message' => array( + '#markup' => $status . ' ', + ), + 'link' => array( + '#type' => 'link', + '#title' => $link_title, + '#href' => $path, + '#options' => array('query' => drupal_get_destination()), + '#access' => user_access('toggle field translatability'), + ), + ); + } + else { + $form['field']['translatable'] = array( + '#type' => 'checkbox', + '#title' => $title, + '#default_value' => $translatable, + ); + } } /** diff --git a/tests/entity_translation.test b/tests/entity_translation.test index 08cf41a..3a3a0e2 100644 --- a/tests/entity_translation.test +++ b/tests/entity_translation.test @@ -133,26 +133,18 @@ class EntityTranslationTestCase extends DrupalWebTestCase { function configureContentType() { // Configure the "Basic page" content type to use multilingual support with // translation. - $this->drupalGet('admin/structure/types/manage/page'); $edit = array(); $edit['language_content_type'] = ENTITY_TRANSLATION_ENABLED; $this->drupalPost('admin/structure/types/manage/page', $edit, t('Save content type')); $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Basic page')), t('Basic page content type has been updated.')); - // Set body field's cardinality to unlimited. - $this->drupalGet('admin/structure/types/manage/page/fields/body'); + // Set body field's cardinality to unlimited and toggle translatability. $edit = array(); $edit['field[cardinality]'] = FIELD_CARDINALITY_UNLIMITED; + $edit['field[translatable]'] = 1; $this->drupalPost('admin/structure/types/manage/page/fields/body', $edit, t('Save settings')); $this->assertRaw(t('Saved %field configuration.', array('%field' => 'Body')), t('Body field settings have been updated.')); - // Set body field to translatable. - $this->drupalGet('admin/structure/types/manage/page/fields/body/field-settings'); - $edit = array(); - $edit['field[translatable]'] = 1; - $this->drupalPost('admin/structure/types/manage/page/fields/body/field-settings', $edit, t('Save field settings')); - $this->assertRaw(t('Updated field %field field settings.', array('%field' => 'Body'))); - // Check if the setting works. $this->drupalGet('node/add/page'); $this->assertFieldById('edit-body-und-add-more', t('Add another item'), t('Add another item button found.'));