Solving your multilingual navigation issues with Entity Translation in Drupal 7

UPDATE: the D7 entity module has changed, and now has support for menus. See my latest post on the topic for more up-to-date information.

This blog post has been a long time coming. I have been using Entity Translation, Drupal 7's new interface to doing translations, for the better part of this past year. It has been an exciting adventure, but with every new way of doing things there are some dragons along the path. In this post, I think I have solved one of my last great challenges with it by using some new contrib modules, so I'm documenting my experience here so you do not have to have the same troubles.

Entity Translation, for those who are not aware, is Drupal's transition to translating fields rather than translating nodes. You can read more about it on Gábor's site on Drupal7 Multilingual if you wish (you've probably already seen this page if you were searching around and landed here).

So what is wrong with Drupal's navigation menus when you use Entity Translation?

  • By default, there is only one navigation menu configuration for the node - on the source translation only.
  • The modules relating to entity translation are very new - some of them even lack a user interface!
  • Menus become a difficult problem because they are not entities like everything else
  • "Language neutral" (aka 'LANGUAGE_NONE' or 'und') nav menu entries must be translated from the default language, even if the source node is not in that language, potentially making your translation tables very messy (not to mention this is complicated to setup).

What is the best way to address these issues?

This new module solves a lot of problems by allowing us to add a field to each node that we will use in place of Drupal's standard navigation "menu_links". It then generates Drupal's standard links managed by this module, so that you can use menus in the standard way.

There is one limitation which had me scratching my head for a couple days. If you set the menu field to "translatable" you can get the menu setting on all versions of the translation. This is fabulous. What doesn't work is taking the language from the translated entity and putting that language setting into Drupal's standard "menu_links" table. Not ideal right? Well, there is a workaround.

You see, it is possible to create a navigation menu in Drupal that is language-specific. So, if you take a step back and think about this, since each language version of an entity-node has it's own menu configuration, why not just localize the menu entirely? That way we don't care if the menu_link is language neutral. With this structure, as an example, the French editor when doing the menu settings on a node must only remember to place the French translation within the context of the other French postings. That is simple enough to explain to your users.

Other dragons? Other awesome things?

There are not that many issues I have found yet since this module generates real Drupal menu_links for the navigation menu you can use any modules that support using Drupal's nav menu system, but you might have to do it twice (or more, depending on how many languages you are supporting.

Additional things you might want to think about:

What about the future?

Eventually, when we get around to adopting Drupal8, menu navigation will probably be based on entities. So we're already inching toward the future by embracing entities for everything now!

UPDATE: You may also be interested in my new Entity Translation Tabs module!

UPDATE 2: Keep in mind you don't need many i18n modules with this, and that some i18n modules cause a conflict. Only "Block languages" (i18n_block) and the core i18n modules are necessary for this configuration.

Be sure to disable i18n multilingual select, i18n field translation, i18n path translation and i18n menu translation.

For your convenience:
drush pm-disable i18n_select i18n_field i18n_path i18n_menu

UPDATE 3:

i18n_taxonomy is OK. i18n_taxonomy might cause troubles too, but I have not tested this yet.

I recommend the "Localize" option on your taxonomy, which requires you to put the default language version first, and the "title" module to replace the taxonomy term name interface. You may have to hook_form_alter your Views if you output your taxonomy as an exposed filter to put the translation.

I put the following into my hook_form_alter function to make taxonomy make sense:

   
  if ('taxonomy_form_term' == $form_id) {
    drupal_set_message('Taxonomy terms must be set in English first.  Then click "save and translate" and you can put the French version.  You can then manage existing translations by going to the term using the translate tab.');
  }
  if ('views_exposed_form' == $form_id) {
    global $language;
    // this uses French, but you could abstract it a bit.
    if ($language->language == 'fr') {
      $result = db_query("select * from {field_data_name_field} where language='fr'");
      foreach ($result as $row) {
        if(isset($form['field_series_code_tid']['#options'][$row->entity_id])){
          $form['field_series_code_tid']['#options'][$row->entity_id] = $row->name_field_value;
        }
      }
    }
  }

Deploy Drupal's blocks on a different instance using Features without having to re-map your CSS

Get the fe_block module as part of Features Extra.

drush dl features_extra
drush pm-enable fe_block

Go to your blocks page. Configure any custom blocks you are planning to deploy. When you edit a custom block you will now have a "machine_name" variable. Give your block a machine name.

Now the block is available for Features export. You can now go to Features in your Structure menu and generate a new feature with the custom block(s) and their settings (placement in the theme, if it will be the same theme on the target system).

This is great so far, but how will we keep our CSS consistent when the block's ID will change? There is a block_class module but it does not export data to Features. Sad face... but wait! We have a solution:

We can do a custom block template in our theme to add the machine_name as a custom class for this block. So the machine_name for each block will now (1) enable the export and (2) get a class of the same name if the theme supports it.

Copy over your block.tpl.php file to your theme's templates folder.

If you have this file in your theme already, great! If you're using a starter kit theme, look in the starter kit's templates folder for this file, and copy it to templates/block.tpl.php in your theme. If you are coding your theme from scratch, grab the original block.tpl.php file from modules/block/block.tpl.php under the root of your site.

Now that you have added that file to your theme, using the name templates/block.tpl.php :

Find the line that defines the 'class' in HTML. It is probably really close to the top.

Put the following line before it. This code will add the machine name as a class while leaving the rest of the code in-place:
<?php $classes .= ' ' . _fe_block_get_machine_name($block->delta); ?>

Be sure to test this code before you put it live! Otherwise, get to work on your CSS using the machine_name defined by fe_block module! Enjoy never copying and pasting your blocks ever again!

Overriding Drupal7 node display with Views

Back in the Drupal6 days overriding node displays with a View was a pretty common thing for me but since using Drupal7 most of the time it is not necessary. That time has finally come, and I did a bunch of searching around for a better/automatic way but I did not find anything that did not involve over-engineering so I did it in my theme.

First, create a view, as a block (to not to have extra pages floating around).

  • Set the title to %1 to set your view title the same as the node
  • Do not use a pager
  • Display only one item
  • Add a Contextual Filter using the NID field, set the fallback behaviour to "hide"

Then, override the node.tpl.php file in your theme. If you do not have this file in your theme, find out which theme your custom theme is based on, and copy over the relevant file to the correct location for your theme.

In your node.tpl.php file, look for the following code:

print render($content);

...and replace it with a view_embed_view.

print views_embed_view('node','block_1', $node->nid);

In this example, the first parameter, node, is the machine name for my view, and block_1 is the first block in the view. When you are editing your view you will probably see both of these parameters in your URL. Finally, the last parameter is the node ID. This last parameter will probably be the exact same that you will use unless you are doing something truly crazy (like showing the wrong node on purpose? har har).

I only wanted to apply the view to nodes which use the 'article' content type so I combined the old behaviour and the new behaviour with a test for the content type:

if ($node->type == 'article') {
      print views_embed_view('node','block_1', $node->nid);
    }
    else {
      print render($content);
    }

There you have it! Now you can use Views to render your fields for a specific node type... and using Views you can customize your HTML as much as you like using the awesomeness of Views without the overhead of Panels or Display Suite.

Using simplehtmldom API with Drupal to radically change node editing UI

In mid 2011 I took on an interesting code challenge and never got around to posting about it. The technique I describe here is available as part of my Drupal 6 module translation wysiwyg if you would like to see a demonstration of the result. This blog post talks about the way we use simplehtmldom API module to traverse the node body content produced by a wysiwyg editor - and pick out all of the translatable elements which we then render as individual fields in the node editor UI.

Still following? Awesome.

What simplehtmldom API does

This module brings the PHP Simple HTML DOM Parser into Drupal for use with your custom modules. It renders all of your HTML that you feed it as a tree of objects that you can perform operations on. If you have used JavaScript and/or JQuery you will probably feel somewhat comfortable working with it. It provides simple dom traversal, and then re-assembly of the HTML all in your PHP code.

How and what we want to parse

In the translation wysiwyg module we want to take the code from the default language version of a node and break it into strings.

  • The goal here is that editors of the default language will get their usual WYSIWYG editor.
  • Editors of translations of the node will get individual fields for each string in the body text.

So our module will have to look at the node before you begin editing to recognize if it is going to be a translation of the original node. If it is a translation we modify the node edit form.

To get the node editor to do what we want we need to do all of the following:

  • Find the default language version of the node and grab it's body text
  • Use the simplehtmldom API to find all of the h1, h2, h3, h4, h5, p and a tags that contain text
  • Check the values contained in each of those tags to see if they exist in the locales database tables
  • Render a tree of Drupal Forms API textareas for each of the text-containing tags listed above
  • Load the translated versions of the items found in the locales table as default values in Forms API
  • Unset the body field so that it does not appear

How we want to put it back together

The obvious problem that we're going to run into with all of these new form fields on our edit form is that we now must re-capture all of the items in the fields and put them in the appropriate places.

Re-enter simplehtmldom API!

Here are all of our steps to re-create the structure of our HTML body content while preserving all of the images, hr tags, object tags... all other tags!

  • Grab all of the submitted fields during the validation of the form
  • Re-load the body text of the default translation
  • Crawl through the tree of the original text, replacing each h1, h2, h3, h4, h5, p and a tags that received a translation
  • Each translated string is stored in the locales table for future editing
  • The new body text is taken from simplehtmldom and converted back to HTML
  • We put this new HTML back into Drupal's node body field and pass the results to the submit function
  • Drupal saves the "translated" version of the node

Note that for any images or custom HTML you put into your original nodes - translators did not have access to change any of that stuff. Only the text.

If you read this carefully you noted that we are now putting a huge sub-set of node body text into Drupal's locales table. This means that your translators could find these strings while searching within the translation interface - however they would not update the node content until the next time someone edits that node and thus loads the new default value for that header, paragraph or anchor tag they modified.

Where this method is really handy is when you have a translator return to a node after the original has been updated. If a new paragraph was added to the node, the only thing to translate is a blank field where the untranslated content occurs. This can be extremely handy.