Drupal Articles

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.

First Drupal code release - Node Tasklist

Yesterday marks my first public foray into developing contrib modules for the Drupal project. I applied for project maintainer status for my new module, Node Tasklist.

From the description of the project:

Each time you visit the page or block created by this module you are presented with the edit form of the most recent node of that content type. When you save your changes, you are returned to the same page so you can perform another update to the node.

Read the description of Node Tasklist in my Drupal.org sandbox.

I made a demo of the module at the end of Montréal's Blitzweekend, where I put the final touches on the code.  You can see the module in action here: http://weal.ca/tl/blitz

I really love doing work that has a time component.  I have in the past written shell scripts and scheduling systems.  I also have a time tracker I developed in-house with Drupal and much more.  I hope to release all three of those as community projects once the Node Tasklist module is fully polished.

Now supporting Drupal on IRC

Yes you read that headline right. I was using IRC in the 90s and it is back in my life in 2011. Drupal maintains a number of channels on http://irc.freenode.net that are used for support and discussion.

I joined IRC as a way to assist others who are just beginning their Drupal adventure. These days I do not need to do as much research. If I encounter problems I just look at the code. Things have become much easier than it was in the early days.

Want to join Drupal's IRC community? Just read the rules.

If you're on freenode I am registered as user weal. Running a /whois you'll see I connect from montreal.weal.ca.

Drupal's Theme Registry Rebuild: turn it off!

Drupal's Zen theme starter kit includes this nifty feature to rebuild your theme each time you visit. It prevents caching so you always see your fresh new code. For a developer this is some handy candy.

Rife with warnings about performance if the settings actually appear, you should take note that if you are using Zen you need to explicitly disable this setting (yes, even if you can't see the option for it).

On one of my larger client sites I found that due to a misconfiguration in the theme-settings.php file the options for the theme were not showing up. You can normally find this caching option on your zen-based theme's configuration page.

Upon doing a find-and-replace in my theme-settings.php file and unchecking the "Rebuild theme registry on every page." option in your Zen theme things should be a lot faster on your website.

The graph above shows the impact this change had on my server. As you can see, the site in question is by far my biggest traffic driver on this host and it was wasting a LOT of database traffic (note the drop off at around 3pm, it should be pretty obvious).

Also be sure to check your "performance" settings under "site configuration" to enable css caching/compression for an added kick.

The Drupal FormAPI Rapid Development Quickstart

This article is based on a presentation I gave to help a friend who is new to FormsAPI.  It will introduce you to a few concepts using Drupal's command line tools.
 
For the better part of the past year I have been working with Drupal's FormAPI rather intensely.  The projects have now spanned a number of different applications.  Some interacting with the user system, some with sessions and others with the database.
 
FormAPI can be notoriously cryptic for developers new to Drupal but once mastered they are a dream to work with.  
 
Surfing online documentation was difficult for mastring this.  Much of my knowledge did not click until I read Pro Drupal Development published by Apress.
 
Today's Project
 
We need to build a simple webform using Drupal's FormsAPI. It will take an action based on the user's selection and send them to another page when complete.
 
It could be done with webform or CCK, but we want to do this faster than with those tools... in code!  
 
Our Assumptions
 
I prefer working at the command line, so that's all we're going to cover here.  I'm assuming you're at your shell now and that Drush is installed.
 
Let's Prototype Faster
 
Make sure you're working in a site that isn't live... your scripting could bring the site down otherwise.
 
Then get over to your website's filesystem and begin:
drush dl module_builder
drush pm-enable module_builder
drush mbdl
Woah! What just happened?  You just downloaded and installed the module builder and then ran it from Drush to get it to fetch all the known hooks that you can tap into with your programming.  The command "mbdl" is an extention to Drush that came with module_builder.
 
Before we begin coding the module we should know what hooks we want to use/modify with the Drupal framework.
drush hooks
You might want to use more or less to scroll through that list if it is too big:
drush hooks|less
If you are new to this you probably don't have a clue of what you need.  I'll give you a list of starter options in a second, but first let's see what happens when we choose just one of the hooks:
drush mb my_custom_module menu
You just asked module builder to prototype a new module called my_custom_module and to add Drupal's init hook.  Init runs on every page so if you need to do some processing on every single page this is the way to do it.
 
The result should quickly scroll onto your screen:
Proposed my_custom_module.module:
<?php
// $Id$

/**
 * @file my_custom_module.module
 * TODO: Enter file description here.
 */

/**
 * Implementation of hook_init().
 */

function my_custom_module_init() {
}
 
That should be enough to get you doing Drupal-style code rather than putting random PHP everywhere.  
 
Usually I also need Drupal's "menu" hook, it notifies the system of any pages I am creating.  It isn't necessary for what we are doing but I plan on extending the module later so I put it in anyway. I also like to include the "perm" hook and add my own permissions settings.  
 
We're ready to build the prototype now.
drush mb my_custom_module init menu perm --write
Note that last parameter. It creates the module folder in (probably) your sites/all/modules folder, complete with a pre-crafted .module and .info file.
 
I commented out menu in the my_custom_module.module file until I have a page function ready to go.  I use init for some debugging on occasion so I'm keeping that in the mix.  Permissions will be rolled out later but we already know who needs access so we put the control into the module early.  We will implement the settings function at a later date.
function my_custom_module_perm() {
  return array('manage my custom module stuff');
}
This adds an entry to our permissions page.  Note: we are not implementing anything that refers on it in this article.
 
We're ready to create a form now.
 
First create some functions.  As of this writing module_builder doesn't seem to have any draft FormAPI forms for us to use and the form hook has the wrong parameters (they apply to nodes, I just want a form!).
 
function my_custom_module_form($form_state) {
  return $form;
}

function my_custom_module_form_validate($form, &$form_state) {
}

function my_custom_module_form_submit($form, &$form_state) {
}
That should be enough to get us started.
 
Now we just need to put something into each function to make our form. I have consulted the Drupal FormAPI Reference to learn that "radios" are the type of form field I want.  That page also lists a bunch of options that apply to the "radios" type. 
 
Let's define the form function with a single field with two radio buttons:
 
function my_custom_module_form($form_state) {
  $form['choices'] = array(
    '#title' => t('Choose the item that is right for you'),
    '#type' => 'radios',
    '#options' => array('1' => 'Product 1', '2' => 'Product 2'),
    '#required' => TRUE,
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => 'Seriously submit!',
  );

  return $form;
}
 
That "#options" parameter contains all of the options that will make up our radio button list.  When processing, selecting "Product 1" will appear to us as simply "1" on our end to save us some typing in the processing stages.
 
I left the validation function blank in my rapid development project.  I usually use it with multi-page forms where I need to store data between pages.  It is also a great place to do error checking.  Is that phone number numeric?  Yeah, you'd check that here.
 
The submit function has a few neat tricks in my final version:
 
function my_custom_module_form_submit($form, &$form_state) {
  if($form_state['values']['choices'] == '1') {
    uc_cart_add_item('5713', 1, NULL, NULL, FALSE);
  }
  if($form_state['values']['choices'] == '2') {
    uc_cart_add_item('5686', 1, NULL, NULL, FALSE);
  }

  $form_state['redirect'] = 'cart/checkout';
}
You Übercart junkies will appreciate this one.  If you picked "Product 1" you will get the site's product (nid) #5713 added to your shopping cart.  It adds 1 item without any messaging.
 
If you pick "Product 2" you will get item #5686 added to your cart.
 
Those product numbers are the node ID (nid) for each product.  If you aren't sure number to use to add to your cart, go find the product on the site and click edit.  See the number in the URL?  That's the node id.
 
Once it's in your cart the form sends you to the checkout page by setting the form redirect.    
 
Last Step - Use the Form
 
You have to put the form somewhere.  For this demonstration I used a "page" template and changed the input format to PHP to run it.  You could also put it in a block using the PHP input filter, or alternatively just add an entry in the "menu" function we talked about earlier.
<?php
print drupal_get_form('my_custom_module_form');
?>

 

Customizing FCKeditor on Drupal

Editing modern CMS-based sites usually means a bit of HTML editing.  More often than not there is some kind of editor built-in with the application framework you are using to facilitate this.  On Drupal you choose whichever editor you think works best.  For me the choice was simple - FCKeditor.  Why FCK? It supports a lot of browsers and is built in a modular fashion.  So there are many different ways you can use it.

Before we get started there are a few decisions to make about how you want to integrate with Drupal.

Choosing Your Module

You can use the "Wysiwyg API" module if you are interested in using the editor for everyone of a specific input format (think "Filtered HTML", etc).  This new module allows you to download the current FCK codebase and permission against that and only that.  It is limited but growing in popularity due to the number of other editors that are supported by this module.  If you need more than one editor this is your best bet.

What I have found in practice though is that you will probably want the FCKeditor module rather than "Wysiwyg API" if you want to customize with a GUI interface and/or set permissions based on URL and/or another method.  For most new installs I use "WYSIWYG API" for it's simplicity.

If your intent is to use custom buttons for page break and to manually set teaser length I would recommend the FCKeditor module - it has the Drupal custom plugins included.

Getting the Code

Once your module is installed you will want to download the source code for the editor.  This will effectively activate your module and allow you to set permissions so you can use it.

Make Editing Pleasurable

By default there are a few settings that are targeted to yesterday's editing preferences.  I like to change the following to better utilize my browser's built-in functions.

  1. Simplify the toolbar - by default FCK shows absolutely everything under the sun.  That means 4 or more bars worth of buttons.  Simplify that!  This can be done in the fckconfig.js file by modifying the tokens in the toolbar.  You can also add new toolbars, when using Wysiwig API you will need to activate new toolbars by editing fckeditor.js to specify the default.  With the FCKeditor module you can use the Drupal admin page to specify the default.
  2. Activate the Drupal Plugins - the FCKeditor module comes with the Drupal Plugins.  These two buttons will set your teaser length and/or page break points.  If working on a site which requires these features the buttons are worth it. (Alternatively, you could copy the plugin from the FCKeditor module's folder to your Wysiwyg API folder and then add the name in your fckconfig.js file if you aren't using the FCKeditor module).
  3. Switch to the Silver skin - the default of all FCK instances is to display a yucky brownish color to match an old Microsoft Office theme.  Not all of my users are on Windows so I switch to "Silver" when I can.
  4. Disable the right-click context menu - when you're editing text in a web browser you often need the real right-click menu rather than one supplied by JavaScript.  Why? Some browsers block the JavaScript code.  Also, Firefox lets you change languages with this menu - quite important for spell checking.  You can change this in fckconfig.js as well.
  5. Disable built-in spell checking - Most editors like this do a terrible job of spell checking anyway.  Modern browsers do a better job on their own.  Let the browser manage spelling like you know it should.  Again, fckconfig.js will get you there.
  6. Whitelist any odd HTML notation you use - one of my projects involved adding <span> tags into the editor space.  Not ideal code, I know, but it is worth noting that you can override the editor to allow certain tags through when it cleans up the code.  Yes, FCK cleans up broken HTML code.  It even has options in fckconfig.js for removing Word-style formatting.  What bliss.

Those changes will probably make you a hero with most offices.  FCK is widely supported so if you get it working it just works.  It has been around for a long time and seems to have pretty good momentum.

When you want to move on to bigger and better things I suggest creating your own plugins for FCK.  They can be complex, but they are well worth the struggle when you get them working.  I use a custom plugin to produce buttons which insert pre-defined text into the HTML.  More on that in a future posting.