If field translatability is switched when a field already holds data, field_attach_form() might stop behaving correctly since it expects that all translatable fields match the entity language. This will cause empty widgets to be the displayed on the entity edit form.
We need to update field languages when switching a field's translatability to ensure that translatable fields match the entity language and untranslatable fields are always set from the entity language to LANGUAGE_NONE.
Example:
<?php>
$node->language = 'en'
$node->body = array(LANGUAGE_NONE => array('value' => 'test'));
// the body field language should be changed to
$node->body = array('en' => array('value' => 'test'));
$node->language = LANGAUGE_NONE
$node->body = array(LANGUAGE_NONE => array('value' => 'test'));
// the body field language should not be changed since it already matches node language
?>
When switching a field from transatable to untranslatable we should worry about multiple language values being held by a field. In this case only the original values should be marked as LANGUAGE_NONE while their translation should be left alone. We should have an option to delete them while updating original values.
The process above should kick-in when updating a single field's translatability (probably we need a batch process). We need to code it to be reusable so that an additional UI can be built to mass change all field's translatability from a single screen.
Comments
Comment #1
dergachev commentedAccording to plach, steps to reproduce:
* install d7 (latest)
* enable locale (nodes will now be assigned default lang, not LANG_NONE)
* create a node
* enable entity_translation, mark a field (ex: body) translatable (initally it's not translatable(
* then in node edit form, notice that existing value is lost (though still exists unreferenced in the DB somewhere)
Comment #2
dergachev commentedI was able to reproduce above, although noticed that using "drush generate-content 1 0" will generate nodes with language LANGUAGE_NONE rather than site default, even after the locale module is enabled.
Comment #3
plachComment #4
dergachev commentedworking on it.
Comment #5
getgood commentedevolving web and I have got the issue worked out for nodes; you can switch translatable and back fine, nothing gets lost. It still needs to be generalised to all entities which have a language attribute.
Comment #6
loganfsmyth commentedSooo, I have a pretty big list of things, sorry :P
@plach Has there ever been any talk of making language a first class property and having it be part of all this stuff?
e. g.
With that, you could also have language for entity_create_stub_entity and in results from EntityFieldQuery.
I think that would require language to be a field in to db though.
Comment #7
getgood commentedI've rerolled the patch to deal with the first few points and make life easier.
To answer a few of the other points:
For the rest, I'll start back on the code soon and address those then.
Comment #8
getgood commentedHere's take 3.
I've addressed most of the issues Logan brought up and generalised the patch to all entities; it no longer makes any reference to nodes in particular. There are still a few major considerations though:
I'm sure there's more to fix, but those are the big ones.
All thoughts and criticisms are welcome.
Comment #9
plachThanks! Hope to be able to review this tomorrow night.
Comment #10
getgood commentedLogan pointed out a much better way to get the list of entities which I think should be okay with revisions. Here's another take.
Comment #11
getgood commenteddidn't mean to change the status...
Comment #12
loganfsmyth commentedJust for reference, I've created an issue for the EntityFieldQuery count bug that you had to work around.
#1292922: EntityFieldQuery count query broken for translatable fields
Comment #13
plachI ain't sure the batch API is being used the right way here, but I might be wrong. Since the Title module performs a very similar operation (acting on all values of a particular field, only of a given entity type though), would you please check how it's done there and if it would make sense here? See lines 273-349:
http://drupalcode.org/project/title.git/blob/refs/heads/7.x-1.x:/title.m...
In particular I don't think the operations array is intended to be populated with every step of a particular operation, but with different operations that can be divided in various steps.
Comment #14
getgood commentedyou're right about the batch api. The comments starting at line 4010 in form.inc are very similar to your example with more explanation.
Here's a version with the batch done properly. I think the bulk of what's left is making the confirmation form and moving most of the code I've added into entity_translation.admin.inc.
Though there is still the issue of how to decide the language of entities...
Comment #15
plachGreat work! We seem to be almost there: after fixing the stuff below I guess the only pending thing will be the confirmation page. I think we can postpone revision handling for now.
We might want to use a warning style here. Also the warning about losing data should be showed only when making a field untranslatable. Moreover ususally we don't use uppercase to emphasize important sentences. I guess something like the following would be ok:
To avoid hacks making the submit handlers work, I tested pushing the submit handler on the front or on the back of the submit handler array depending on the field's translatability:
I'd use $translatable and $previous respectively for consistency with other parts of ET.
This part can go away if we set the submit handler order conditionally as suggestd above. However to make this approach work we must work around a bug I discovered while reviewing this patch (see below).
Also this can go away.
The related issue has been committed, so we can use a plain count efq here.
Please remove the dsm() calls before rolling the patch.
These two loops can be merged.
The correct way to handle the entity language is using the translation handler. Unfortunately we need a full entity to instantiate it. The good news are that now entities are optimized for multiple loading and that the entity cache module speeds this up even more:
As per above this can be replaced by:
($langcode is the variable name commonly used)
This cannot work :) $content should be replaced by $context. However incrementing the progress key by $inc (I think the batch API example uses the $limit name btw) is not correct since we might go over the max value. I'd simply increment the progress key by 1 in the previous foreach loop. This should allow to remove the if check.
To workaround the bug I mentioned before we need to add the following lines here:
See #1300928: Form submit handlers not called during batch processing if not already included.
This can go away.
This can go away.
Aside from the things above, there are some coding standard issues with comments: they should always start with a capital letter and end with a dot. Moreover they should wrap at column 80. See http://drupal.org/coding-standards for details.
Comment #16
plach(bump)
Any news?
Comment #17
dergachev commentedSorry, it looks like Thomas doesn't have email notifications setup properly. Just pinged him about this.
BTW awesome review.
Comment #18
getgood commentedLot's of good advice, thanks plach.
I've implemented most of your suggestions and started on the confirm page but got side tracked. I'll clean up what I have and try to wrap it up tonight if I can. I'll put something up either way. I had an issue with loading the translation handlers, but I think I know what I was doing wrong. I'll let you know soon if I in fact don't.
Comment #19
getgood commentedSorry for the delay. I was trying to get the confirm page working but I haven't had much time and am having some trouble with getting the redirect to work properly.
As it is I've implemented all of your suggestions and cleaned up a bit.
Given that this is major and I'm not exactly speeding along, I'll understand if someone wants to take over. If not, I'll keep at it as I have time.
Comment #20
plachMinor code style fixes. Confirmation dialog stil missing. Let's test this.
Comment #22
getgood commentedAfter an absurd hiatus, another attempt to please the testbot. I've got some time now, so hopefully that confirmation dialog will be done soon too.
Comment #23
loganfsmyth commentedFor testing.
Comment #24
loganfsmyth commented* Try not to include big hunks of commented out stuff in patches. I usually use git add -p to build up patch contents, the use git diff --cached to output the patch.
* Could we add some explanation of why it is important that the translations get moved before the field becomes untranslatable? The comment by the #submit stuff is kind of vague.
* Can't the translatable submit just always run before the field config is updated? Be we need to two conditions?
* Plach said above to ignore revision handling for now, so maybe it can show a warning at least so it is clear?
** EntityFieldQuery is only going to look at the current revisions. You'll need to do two EntityFieldQuerys with different ages.
** http://api.drupal.org/api/drupal/includes--entity.inc/function/EntityFie...
** This will also mean a change in the way entity_load is executed.
* Need to call field_attach_presave too. The blog post you linked
* Would it be a good idea to put the site in maintenance mode while this batch job runs? I feel like things could go wrong if it is running while people try to edit content. Is that something we is just left up to the site author to do?
Comment #25
plachThanks Thomas! Besides Logan's comments, which look sensible:
As Logan's comment shows, it's not immediately clear why we need two different orders: a comment here saying something like "Since a field translatabilty determines which values can be retrieved from/saved to the storage, we need to ensure we perform the needed changes after/before we enable/disable translatability to be able to deal with multilingual values." would be fine.
I'd trim the whitespaces around the emmed part. Also, there is a double dot.
Trailing whitespace.
Wouldn't
empty($count)work here?Typo.
About
I ain't sure which is the recommended behavior: I'm not aware of modules silently putting the site in maintenance mode (but they might exist :)). Moreover, this really look like an operation one would do in a dev environment or in maintenance mode anyway. Perhaps a warning could be enough?
Comment #26
getgood commentedBefore I get carried away, here's the cleaned up version.
I used intval($count) === 0 instead of empty($count) because it's more obvious that way what's actually being checked. If you think that will cause problems, I can change it.
Comment #27
loganfsmyth commentedis just highly verbose when you could simply be doing this.
Comment #28
getgood commentedThe whole system of what's true and what's false in php bothers me; I like to know what I'm checking. But I'll change it for the next round.
Comment #29
getgood commentedI've got a confirm page working, though I'm sure it needs some work.
The cleanest way I could think of after talking to Logan was to get rid of the checkbox completely and replace it with a link to a confirm page which runs the batch op. This breaks some of the tests, but I'm loath to change them until I get some feedback on the flow I built.
The code also breaks when I refactor it into admin.inc, but that's comparatively minor.
Comment #31
getgood commentedForgot to pull first. Try again.
Comment #33
plach#31: et-translatable-switch-1279372-31.patch queued for re-testing.
Comment #35
getgood commented#31: et-translatable-switch-1279372-31.patch queued for re-testing.
Comment #37
getgood commentedI've modified the tests to check the new form instead of the no longer extant checkbox.
Comment #38
loganfsmyth commentedQuick review of the code. This looks great, just have a few comments on the way the form is built up, and how the data is passed around.
This menu path will only work for Nodes, so the menu path should probably be something more generic like 'admin/config/regional/entity_translation/translate/%', with % as the field_name. You don't need the bundle for this conversion.
Better to do
$form['#submit'][] = 'entity_translation_form_translatable_submit';Rather than saving the whole field here, I would recommend adding two items to $form, 'field_name' and 'translatable' as #type => 'hidden'. That way your intent is more directly sent when you submit the page. Otherwise, when you submit the page, you are essentially just saying 'toggle', which might conflict.
This lookup is a bit weird. Is there nowhere in $form or $form_state that you can get that is a more straightforward way?
field_info_field($field_name) is what you want here.
There is a #type => 'link' that might be better to use here. You can wrap it in the div with #prefix and #suffix.
As I said in my comments above, it would be better to read these directly from $form_state. And be sure to do another field_info_field and make sure that translatability isn't the value you want.
field_info_field() again.
Comment #39
getgood commentedClean up as per Logan's comments.
Comment #41
loganfsmyth commentedMenu items are one of the only places where you aren't supposed to wrap the strings in t() calls. The strings are cached, so if you cleared the cache when viewing the site in French, then all the menus wrapped in t() would change to French for everyone in the site.
confirm_form actually overrides $path with $_GET['destination'] automatically, so I would just set $path as '<front>' or something.
If you change your submit function to be named entity_translation_warning_form_submit, then you won't need to have this line at all.
Close, but not quite what I meant. #type hidden is just like any other form element.
Then these would be:
The point of this is so that the values for field_name and translatability are actually stored in the page and are submitted right along with the rest when you press Submit.
As it is now, pressing Submit basically instructs Drupal to take the current translatability setting and toggle it. So say you opened two pages with this form and both said "Submit to make non-translatable" and you submitted one. The field would be non-translatable. At this point if you submitted the other one, even though it said that submitting would make it non-translatable, submitting it would actually toggle the value again and make it translatable.
Comment #42
loganfsmyth commentedI found a bug that won't come up very often, but it's a big one. It's also not your fault.
I make a core issue here: #1380660: field_available_languages static cache never cleared
In a default drupal install, this is not a problem because your form doesn't trigger populating that cache, but you can't rely on that.
If anything in the site triggers the loading of values for the field you are converting, then this bug will cause all the values to be erased. In my testing, I loaded a node revision using node_load(1,1) in a hook_init, and that was enough to erase all body field data.
The simplest fix for now is just to add this after your field_update_field:
Comment #43
loganfsmyth commentedI'd also just like to point out that this all works because LANGUAGE_NONE is one of the available languages for translatable fields. If for instance someone implemented hook_field_available_languages_alter and removed LANGUAGE_NONE, the conversions in this patch would fail in a few different places causing data loss.
It would be a good idea to put in some verifications that field_available_languages contains the languages being converted to and from.
Comment #44
loganfsmyth commented#1380660: field_available_languages static cache never cleared has been fixed in 7.x and 8.x. It still makes sense to clear that cache yourself after calling field_update_field for now, in order to support older versions of 7.x, but add a comment so it can be removed at some later time.
Comment #45
getgood commentedI think I've covered everything Logan said.
I also refactored the bulk of the logic into entity_translation.admin.inc.
Comment #46
loganfsmyth commentedSorry, I realize that I recommended this, but I think that since $field_name is an argument in the URL, it is not needed to have this one in the form as a hidden value. This value you CAN pass in $form['#field_name']. 'translatable' definitely should be here though.
Missing a space between the "." and "'".
We need to add some safeguards here to try to be 100% sure that the data will actually save properly. I think before calling field_attach_update you should confirm that the new field language exists in the results of field_available_languages. @plach, do you think that's necessary?
I don't think this does anything. Any idea why it was needed before? drupalPost automatically does a GET before processing the form, so this is just duplicate logic I think. If you want to pass a destination for the POST, then the fourth param of drupalPost is options for the GET.
http://api.drupal.org/api/drupal/modules--simpletest--drupal_web_test_ca...
Since this form now has those the hidden variables, we should include actual values in the second param of this POST.
Comment #47
getgood commentedI've explicitly set translatable in the test and moved field_name out of the hidden field. As for the others, the GET request helped me find a problem at one point and it's the general structure of the whole test file, so I'd rather not change that. As for the double checking that the language being translated to exists, I understood that the translation handler must have an enabled language. If I'm mistaken, I still think it would make more sense to ensure that --- if it's reasonable --- so that things are consistent.
Comment #48
plachThanks for keeping this issue going!
Edit: updated review :)
Surplus empty line.
Usually we don't recommend to backup data in confirmation forms, although it can be a good practice. I'd remove the last sentence and turn the warning into an explicit question. We might just want to reuse (more or less) the same confirmation message appearing when multiple nodes are going to be deleted.
Wrong comment wrapping.
Can we use something like "switch data back" instead of "convert data back"? It somehow clashes with "Conversely".
Can we add field name and translatabilty in the title here?
I'd like to have a consistent variable name: either
$make_translatableor$translatable. No strong preference about which one.Please move the empty line below the closing brace.
Let's use a system variable here, so this value can be configured. Something like
"entity_translation_translatable_batch_limit"Wrong comment wrapping.
This is not completely correct: we need to check whether there is no translatable field left before removing an entity translation.
We need a
field_attach_presave()call before saving the field values.Missing spaces around the
/operator.Let's make the PHP doc reflect what the function actually does: I'd remove the second sentence until we actually turn the
@todobelow into code.The
@todoform should be used.This belongs to
entity_translation_menu(), let's move it over there and remove themodulekey. The other admin item declaration will follow in another patch. Moreover the 'access arguments' key is wrong: ideally it should match the one used to access the Manage fields page, and every entity type has its own, seefield_ui_menu(). However, to avoid making this overly complex I'd add an ET permission explictly granting access to the bulk update form. We should add an'#access'key to$form['field']['translatable']['link']checking this permission.Comment #49
getgood commentedThanks for all of the feed back. It's getting much cleaner.
I can't find a good way to check that an entity has no translatable fields left before deleting the translation handler. I've put in a clumsy iteration over all of the fields that its type has, but that's not very efficient. Should we consider letting the translation handler keep track of that sort of thing? or is that too far beyond its original purpose?
Comment #50
loganfsmyth commentedFew very quick comments.
$entity->type will only work for nodes. You need to specifically look up the bundle id.
Oops :P
Comment #51
plachMinor thing: the actual stuff being deleted is the entity translation, not the translation handler.
Comment #52
getgood commentedThat's what I get for posting in a hurry. Anything else?
Comment #53
plachFrom a first look everything seems ok! I'll perform some manual testing later today.
Comment #54
plachAfter a thorough testing session I found out some edge cases that were not handled by #52:
Moreover:
I also changed some UI texts, trying to make them more consistent with the core UI. Finally I performed some cosmetic changes to make code suit my personal taste :)
I'd like to commit this in a few days and then start thinking about revisions. Let me know if everything looks ok.
Comment #55
plachComment #56
plach"old translations"
This should be
'toggle field translatability'.As above
This does not work anymore.
Comment #57
kristen polI had a hard time finding this issue because of its title so I added a new issue and marked it as duplicate of this one:
#1413502: Fields show up empty/blank if field translation is turned on after field has content
I'll test the patch right now.
Kristen
Comment #58
kristen polThe patch from #56 was using two different permission names:
change field translatability
and
toggle field translatability
Since the latter was used more, I've updated the patch to use that.
Kristen
Comment #59
kristen polIt is very hard to notice this option because it is no longer a label and no longer bold. This patch makes the text bold. It still includes the permission name fix as well.
Kristen
Comment #60
kristen polIt wasn't obvious to me how to test but I figured it out by looking at the code...
How to test patch from #59:
I tried out patch #59 and it works for me... but, I can't mark as reviewed & tested since I provided that patch.
I would *love* (love!) someone to try it out very, very soon and get it committed as I'm writing a book on D7 i18n for Packt right now and am almost done with the first draft (as in I'll be done tomorrow or the next day)! I currently have the old checkbox (on the field settings form) in the book exercises. I will need to update the book so that it uses this new method. Pretty please?
Thanks!
Kristen
Comment #61
plachI don't think making this bold is the right thing to do here. AAMOF the checkbox title still gets a normal weight. Maybe we could introduce a
<label/>element as a prefix in both cases.Comment #62
kristen polThanks for the fast response!
I guess it was more obvious before because it had the checkbox. I think we need something to make this more obvious. I'll look at the label option.
Thanks,
Kristen
Comment #63
plachJust a note: the checkbox is still there, but it is showed only when the field has no data. In this case translatabilty can be toggled without having to update field languages.
Comment #64
kristen polThe label looks good to me (better than the
<strong>, definitely!). Here's a new patch and a screenshot of what the label looks like.Kristen
Comment #65
kristen polComment #67
kristen pol@plach - Thanks for the heads-up on the checkbox... I'll make sure to explain both methods.
Kristen
Comment #68
plachAs I was saying above also the checkbox could use a Field translation label, at very least for consistency. Would you mind to create a new field and post a screenshot also for this case?
Comment #69
kristen polWhoops... let's try that again.
Kristen
Comment #70
kristen polOk, well, since there is a checkbox, that really needs the label too! So, sorry for the flurry of patches, but I think this one is the best.
[update: @plach - you are so responsive that I didn't even see your post before I update patch ;) That's a good thing!]
Kristen
Comment #72
kristen polHmmm... trying to figure out what it doesn't like.
Kristen
Comment #73
kristen pol#70: et-translatable-switch-fix-permission-name-label-checkbox.patch queued for re-testing.
Comment #75
kristen polComment #77
kristen polArgh. It's complaining about line 337 which I didn't touch. Not sure what to do here. I didn't reroll the whole patch from scratch... just add the changes... which worked for the first rounds of patches. I'll see about rerolling the thing.
Kristen
Comment #78
kristen polOk, so I've only done simple patches before... I'm assuming I need to follow the guide for using git for making patches?? Looked through it and it sounds fun... ha. The changes are trivial, but looks like I will be scratching my head for awhile trying to get a patch working... unless someone else wants to take a stab?
The changes are:
to
to
to
Kristen
Comment #79
plachComment #80
kristen polThanks @plach! I've been trying to figure out how to deal with making a "p1" patch... I don't have the "diff --git" option on my computer (still running an older version of Linux). I guess I will need to figure this out soon...
I have tested your patch against the latest dev version and it is working as expected:
Thanks!
Kristen
Comment #81
plachOk, I'll wait a couple of days before committing to give a chance to Thomas and Logan, who did most of the work here, to have a final review.
Comment #82
kristen polGreat!
Thanks again,
Kristen
Comment #83
loganfsmyth commentedI feel like this is a bad way to do this. As I mentioned in comment #38 and 41, this separates the operation being performed at render time from the operation being performed at submission time. By simply storing the field object, and not having the 'translatable' option embedded in a hidden field on the page, you change the meaning of the form submission from 'Set Translatable' and 'Set Untranslatable' to 'Toggle Translatabliliy'.
In 99% of cases this is never going to be an issue, but I feel that it's silly just to ignore it, or remove the fix Thomas already had in there. As it is now, if someone opened that form twice then they would both say "Enable Translations" or something. If the user submitted one but forgot or something, then went to submit the second one, even though the page clearly says submitted will make the field translatable, submitting in fact would toggle the translatablility back to untranslatable.
What is the motivation for running _entity_translation_update_field twice per entity? Seems like it should work fine with just one.
When would invalid data be introduced in this workflow? I'm not the biggest fan of calling 'private' field API functions unless it is really needed.
Comment #84
getgood commentedThe reason I chose to delete the translation handler, was that as long as it was around, I was getting odd behaviour when translating nodes.
When you go to translate a node and it has no translatable fields, there should be a message in the right hand column of "node/%/translate" saying "No translatable fields". When the entity has translatable fields, it should be a link reading "edit translations". Now, if you convert the field to translatable, add a translation, and then switch it back to untranslatable, the link in that column reading "edit translation" remains, but it now goes to a blank form because there aren't actually any translatable fields.
If we're not going to delete the handler, then we have to change how the form at /node/%/translate renders that link. I'm not entirely sure how this applies to more general entities, however.
Comment #85
plach@loganfsmyth:
I'd have to check, but if I'm not mistaken the form storage is cached though a build id, hence if a field definition is there, two form views/submits would be idempotent. AAMOF the $field variable would not be updated after the first submit, but would be retrieved from the form cache.
Files are lost when making (file) fields untranslatable because files are deleted when removing the field translations they belong to. Only afterwards the language neutral "translation" is created, but the files corresponding to set
'fid'do not exist anymore.Apparently submitting empty file widgets result in non-empty invalid items:
$item == array(array('fid' => NULL)).Comment #86
plach@getgood:
As I was saying above, the new UI will behave differently and present a full entity form, with all the fields there, so this behavior would be more sensible. Moreover every entity translation has some metadata associated (author, creation, last change): silently deleting it might result in uninteded data loss.
Just a note: it's not the translation handler that would be deleted, but an entity translation.
Comment #87
getgood commentedAs long as that's being considered in the new UI I'm happy. Everything else looks reasonable to me.
Comment #88
loganfsmyth commented@plach
I wasn't 100% sure either, but I just checked to confirm. It does indeed toggle instead of set/unset. I think if we wanted caching then we'd have to set
$form_state['cache'] = TRUE;but it's still adding an unneeded dependency on caching when we could just pass the data through the form.I figured there was a reason for calling it twice and I can't think of a simpler way without fixing the File module.
Since we are only doing entity_load, where do the fid => NULL rows get in there? Weird, is this an issue with File?
Comment #89
plachMakes sense. Sorry for missing this.
Honestly no idea. Probably it's a File module issue, but I'm not very keen to fix the third core bug just to get through this issue :)
Comment #90
loganfsmyth commentedAssuming this all passes, I'm happy.
Comment #91
plachCommitted and pushed to the master branch!
Thanks to everyone working hard here, especially Thomas and Logan: awesome work, guys!
Comment #92
kristen polYeah!! Thanks everyone!
Kristen
Comment #93
loganfsmyth commentedWooo!
Comment #97
andrezstar commentedsorry duplicated post