Consider the following scenario:

(1) create two drupal sites, etsource.localhost and etdest.localhost
(2) on both, enable locale and language, go to admin/config/regional/language, and install and enable French in addition to English.
(3) on etsource, download and enable the features module
(4) on etsource, create a content type x
(5) on etsource, go to admin/structure/features/create, create a new feature with the content type x
(6) now put the module x in etsource and etdest, in the sites/all/modules folder
(7) enable x on both etsource and etdest
(8) on etdest, node/add/x, and enter "created with untranslatable field" as a title and body: this will be etdest/node/1
(9) on etsource and etdest, dowload and enable entity_translation
(10) go back to etsource.localhost/admin/structure/types/manage/x/fields/body, click "users may translate this field" and hit save
(11) go to http://etsouce.localhost/admin/structure/features/x/recreate, add entity_translation as a dependency and save
(12) put the new version of x on etdest
(13) on both source and dest, revert the feature
(14) on etdest, node/add/x, and enter "created with translatable field" as a title and body: this will be etdest/node/2
(15) update the feature again to reset the body field to untranslatable, and deploy.
(16) on etdest, create node/3 with the title and body "created with untranslatable field again".

node/1: you see the content
node/1/edit: you see the content in the body field
node/2: you don't the content
node/2/edit: you don't see the content in the body field
node/3: you see the content
node/3/edit: you see the content in the body field

When deploying new field translatability, an api function in entity_translation might be good, so we could do something like:

function x_update_7001() {
  // this function does not exist!
  entity_translation_update_field_data('body');
}

For now it seems that the function that does this is really tied to the batch process and the form, so there is no clear way to do this programmatically.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

alberto56’s picture

In the above scenario, the database is in this state:

mysql> select entity_id, language, body_value, revision_id from field_data_body where entity_type = 'node';
+-----------+----------+----------------------------------------------------------+-------------+
| entity_id | language | body_value                                               | revision_id |
+-----------+----------+----------------------------------------------------------+-------------+
|         1 | und      | NODE 1 CHANGED                                           |           6 |
|         2 | en       | created with translatable field                          |           2 |
|         3 | und      | created, again, with untranslatable field NODE 3 CHANGED |           4 |
+-----------+----------+----------------------------------------------------------+-------------+

If the field should be LANGUAGE_NONE, we would need the function to update the language of line two of the above, it seems. That way, if a field is being modified during a deployment, our function could make sure the above table would be in a consistent state.

alberto56’s picture

To normalize the data in this table, I used the following code. What I have in mind for entity_translation_update_field_data() is a more robust version of this:

function MYMODULE_update_7021() {
  // I am deploying a new untranslatable version of body, because
  // I had previously made it translatable by error.
  features_revert(array('MYMODULE' => array('field')));

  // deploying it this way, though, does not change the data in the field_data_body
  // we need to to this here
  // see drupal.org/node/1916712
  $query = db_select('field_data_body', 'f')
    ->fields('f', array('language', 'entity_id', 'body_value'))
    ->condition('f.entity_type', 'node', '=');
  $query->innerJoin('node', 'n', 'n.vid=f.revision_id');
  $result = $query->execute();
  
  $update = array();
  
  while($row = $result->fetchAssoc()) {
    if ($row['language'] != LANGUAGE_NONE) {
      if (!isset($update[$row['entity_id']])) {
        $update[$row['entity_id']] = $row['body_value'];
      }
      else {
        $update[$row['entity_id']] .= '; ' . $row['language'] . ': ' . $row['body_value'];
      }
    }
  }
  
  foreach ($update as $nid => $body) {
    $node = node_load($nid);
    $node->body[LANGUAGE_NONE][0]['value'] = $body;
    node_save($node);
  }
}
bforchhammer’s picture

Yes, having something like entity_translation_update_field_data() would be useful for automatic data migration tasks. Ideally that function would also be used by the existing batch process in order to avoid duplicate code/logic.

However, until we get there, note that it should also be possible to use the batch process from within your update function by reusing the hook_update-batch sandbox. I had a very similar problem a while ago, see code below. Maybe it can help you avoid having to update the database yourself... Note that I wrote this about a year ago for the old alpha version of ET, so no guarantee that it still works. :-)

Also note that if you want to use this for multiple fields, I found that you had to use a separate hook_update for each field or otherwise the batch process would get stuck.

/**
 * Enable multilingual property: field_article_title
 */
function MYMODULE_update_7004(&$sandbox) {
  MYMODULE_features_enable_entity_translation('field_article_title', $sandbox);
}

/**
 * Migrate existing field content from language "undefined" to entity language.
 *
 * @param $field_name
 *   Field to enable entity translation on.
 */
function MYMODULE_features_enable_entity_translation($field_name, &$sandbox) {
  $context = array('sandbox' => &$sandbox);
  module_load_include('inc', 'entity_translation', 'entity_translation.admin');
  entity_translation_translatable_batch(TRUE, $field_name, $context);
  $sandbox['#finished'] = $context['finished'];
}

When you start converting an existing site to use ET you probably also want to start using the title module; here's the snippet I used for migrating old node titles to title module ones.

/**
 * Migrate title to field_title.
 */
function MYMODULE_update_7003(&$sandbox) {
  MYMODULE_features_title_field_init('node', 'page', 'title', $sandbox);
}

/**
 * Populate title field with existing title values.
 */
function MYMODULE_features_title_field_init($entity_type, $bundle, $legacy_field, &$sandbox) {
  $context = array('sandbox' => &$sandbox);
  module_load_include('module', 'title');
  title_field_replacement_batch($entity_type, $bundle, $legacy_field, $context);
  $sandbox['#finished'] = $context['finished'];
}

Hope this helps. :)

alberto56’s picture

Thanks, very useful indeed!

Albert.

alberto56’s picture

If one is updating a field from untranslatable to translatable one a single site, the confirmation text warns you that you will be losing data. The same is not true if you are deploying a change -- so it might be prudent to not implement the exact same code: for a deployment, perhaps keeping a copy of the translations might be a good idea in case the field is again set to translatable.

pbuyle’s picture

Uber-useful.

Starting from #3, I wrote a unique hook_update_N() to process all translatable fields, it can be found at https://gist.github.com/pbuyle/51d30e80a17920f6df12.

fago’s picture

Status: Active » Needs review
FileSize
2.75 KB

>Uber-useful.
Indeed!

Starting from #3, I wrote a unique hook_update_N() to process all translatable fields, it can be found at https://gist.github.com/pbuyle/51d30e80a17920f6df12.

That works already pretty good - thanks. I just had to make a few small adjustments to make it work for me. I think the update so far is written fine already and can be added to the module. Thus based on that, here is patch which does so.

fago’s picture

ops, corrected example code in the docs.

fago’s picture

and to make it a bit clearer...

donquixote’s picture

This approach has a problem.
When running the batch BEFORE the respective feature is reverted, it causes data loss and will misbehave.

To be more precise:
if the field is still configured as not translatable, but you run the batch with $translatable = TRUE, then field_sql_storage_field_storage_write() will discard all the language-specific values, while the language-neutral items will be deleted.

donquixote’s picture

Perhaps better would be to provide a drush command, and/or another version of the UI page that does not switch the translatability but only updates the field items according to the current setting.

jcisio’s picture

Re #10: I don't see why patch #9 is a problem. It gives just a function to migrate all data for translatable fields and can be used anywhere if $sandbox is provided. So in a hook_update_N, a features_revert() should be called first. But if it is not called, there is no problem because untranslatable fields are not migrated.

donquixote’s picture

Re #12
You are right.

Calling entity_translation_translatable_batch() explicitly from your 3rd party code can cause damage, if you are passing the wrong value at the wrong time for the $translatable parameter.

Calling entity_translation_migrate_field_content_update() from 3rd party code does not do damage, because it has no $translatable parameter.
The worst thing that can happen in this case is that the migration never happens, because the calling code runs in the wrong order. This in itself does not damage existing content, it could be fixed with a follow-up second attempt at updating.
Perhaps having the field in a non-updated state could cause damage over time from other operations, not sure.

Also, a caller might be tempted to pass the field name and field info explicitly to the function via $sandbox['fields'], to avoid that all fields are being processed even though their translatability has not recently changed. This could have the same problem as calling entity_translation_translatable_batch() with the wrong $translatable parameter at the wrong time.

Note that with all of these problems we could simply blame the calling code, they should be more careful.
But imo we should always try to make it hard to use an API in a wrong or destructive way.

Also note that this function only works for one direction of the conversion. I assume we also need an update operation to make a field untranslatable?

So in a hook_update_N, a features_revert() should be called first.

This is not really advertised on this function, is it?

I have seen "deployment instructions" for custom code where a "drush fr NAME" and "drush updb" had to be called in a specific order. Something I would rather avoid. So calling the features_revert() from the updb seems like a good idea, but it should be documented.

donquixote’s picture

If we advance this patch, it also needs a reroll and then some code style fixes.

Attached is a pure reroll after fixing a trivial conflict. No CS fixes.

jcisio’s picture

No CS fix. Just add features_revert() in the docblock because I think it's the only important point.