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' => '#prefix' => '