Upgrade Your Drupal Skills

We trained 1,000+ Drupal Developers over the last decade.

See Advanced Courses NAH, I know Enough

Features and i18n configuration management, part 2: Menu item madness

This week, I'll describe a particularly challenging component I had to deal with: inoffensive-sounding menu items. Should be easy, right? Well, it wasn't.

I won't get into the basics of creating multilingual menu items here. There is good documentation on how to get that set up using the i18n submodule i18n_menu. To provide a context for this post, I'll just mention that we are manually creating menu items, through the admin UI. We have two types of menu items:

  • Ones that should appear in both languages, localized, and pointing to the same place. Those are created with the language set to "Language neutral".
  • Ones that should only appear in a specific language. We set that language explicitly on the menu item's form.

Internally, the i18n_menu modifies the core menu_links and menu_custom tables by adding, among others, a language attribute to save the above info. In addition, the module creates a new text group called menu to save menu translations. In this group, each translation is identified by a menu name and mlid to refer back to the original menu item.

The first problem occurred even before we introduced Features into the mix. The decision to use the mlid as part of the translation identifier means that, across site stages and instances, menu items should have the same database primary key in order to be correctly translated. This design decision introduced a lot of instability in our configurations, for example preventing us from creating new menu items on the fly, for testing or customization purposes. In essence, we would have needed to stick to an install profile approach to manage the site configuration - which is a desirable goal in itself, but one we didn't pursue. In the mean time, our menu item string translations were only reliably showing on the machine where translation occurred, but not elsewhere. This clearly had to be fixed.

We found the module Entity menu links to solve one half of this problem. This module creates a uuid for each menu item, ensuring that uuid remains unchanged throughout the lifetime of the menu item. This module also modifies the core menu_links table by adding to it a uuid attribute.

Still, the string translations were being saved with the mlid. How to convince i18n_menu to use the uuid instead? That's where we had to write some code. The i18n architecture uses the concept of i18n objects that correspond to Drupal site components. We used the following code to override the i18n object info for menu items to use the uuid as a key:

/**
 * Implements hook_i18n_object_info_alter().
 */
function checkdesk_core_i18n_object_info_alter(&$info) {
  // Use UUID field to identify menu_link.
  $info['menu_link']['key'] = 'uuid';
  $info['menu_link']['load callback'] = 'checkdesk_core_i18n_menu_link_uuid_load';
}

/**
 * Callback to load menu_link by UUID.
 */
function checkdesk_core_i18n_menu_link_uuid_load($uuid) {
 if (!empty($uuid)) {
    $query = db_select('menu_links', 'ml');
    $query->leftJoin('menu_router', 'm', 'm.path = ml.router_path');
    $query->fields('ml');
    // Weight should be taken from {menu_links}, not {menu_router}.
    $query->addField('ml', 'weight', 'link_weight');
    $query->fields('m');
    $query->condition('ml.uuid', $uuid);
    if ($item = $query->execute()->fetchAssoc()) {
      $item['weight'] = $item['link_weight'];
      _menu_link_translate($item);
      return $item;
    }
  }
  return FALSE;
}

Unfortunately, the strings were still showing up with the mlid on the translation interface. After many a WTF incantation, we found that i18n_menu was not honouring the i18n object key attribute while creating the string identifiers. We submitted a patch to fix this.

At this point, we had successfully modified the indexing mechanism of i18n_menu to use uuids, as shown below. Our testing with manual .po files revealed that regardless of mlid differences, menu item translations were being successfully moved from one instance to another.

i18n_menu with UUID identifiers

Now menu item translations were made reliable across instances, but they still weren't being saved in a feature. The core Features module does support menu items, but it does not export the additional attributes we introduced above, namely language and uuid. We also need to export customized because i18n will not translate menu items that are not marked as customized.

We submitted a simple patch to Features that allows a hook_query_TAG_alter to extend the relevant query and return extra fields to be exported. Our implementation of this hook looks like this:

/**
 * Implements hook_query_TAG_alter() for `features_menu_link`.
 */
function checkdesk_core_query_features_menu_link_alter($query) {
  // Add missing attributes for translation.
  $query->fields('menu_links', array('uuid', 'language', 'customized'));
}

After this change, the exported menu links look like this (note the last 3 attributes on each entry):

/**
 * @file
 * checkdesk_core_feature.features.menu_links.inc
 */

/**
 * Implements hook_menu_default_menu_links().
 */
function checkdesk_core_feature_menu_default_menu_links() {
  $menu_links = array();

  // Exported menu link: main-menu:node/add/discussion
  $menu_links['main-menu:node/add/discussion'] = array(
    'menu_name' => 'main-menu',
    'link_path' => 'node/add/discussion',
    'router_path' => 'node/add/discussion',
    'link_title' => 'Create story',
    'options' => array(
      'attributes' => array(
        'title' => '',
      ),
      'alter' => TRUE,
    ),
    'module' => 'menu',
    'hidden' => '0',
    'external' => '0',
    'has_children' => '0',
    'expanded' => '0',
    'weight' => '-47',
    'uuid' => 'edc54df9-4aa8-bf84-dd89-ca0a351af23b',
    'language' => 'und',
    'customized' => '1',
  );
  // Exported menu link: main-menu:node/add/media
  $menu_links['main-menu:node/add/media'] = array(
    'menu_name' => 'main-menu',
    'link_path' => 'node/add/media',
    'router_path' => 'node/add/media',
    'link_title' => 'Submit report',
    'options' => array(
      'attributes' => array(
        'title' => '',
      ),
      'alter' => TRUE,
    ),
    'module' => 'menu',
    'hidden' => '0',
    'external' => '0',
    'has_children' => '0',
    'expanded' => '0',
    'weight' => '-49',
    'uuid' => '0bc3af5d-28a8-c864-bd93-f17d8bea2366',
    'language' => 'und',
    'customized' => '1',
  );
  ...
}

With these patches, we were able to reliably persist multilingual menu links using Features. The menu item translations are saved in the translations component of the feature, as described in part 1 of this series. They look like this:

/**
 * @file
 * checkdesk_core_feature.features.translations.inc
 */

/**
 * Implements hook_translations_defaults().
 */
function checkdesk_core_feature_translations_defaults() {
  $translations = array();
  $translations['ar:menu']['a6b48d33d248c146aa8193cb6f618651'] = array(
    'source' => 'Create story',
    'context' => 'item:edc54df9-4aa8-bf84-dd89-ca0a351af23b:title',
    'location' => 'menu:item:edc54df9-4aa8-bf84-dd89-ca0a351af23b:title',
    'translation' => 'أنشئ خبر',
    'plid' => '0',
    'plural' => '0',
  );
  $translations['ar:menu']['8578b45ff528c4333ef4034b3ca1fe07'] = array(
    'source' => 'Submit report',
    'context' => 'item:0bc3af5d-28a8-c864-bd93-f17d8bea2366:title',
    'location' => 'menu:item:0bc3af5d-28a8-c864-bd93-f17d8bea2366:title',
    'translation' => 'أضف تقرير',
    'plid' => '0',
    'plural' => '0',
  );
  ...
}

Now I need your help to review and support (and possibly enhance) the patches submitted to i18n and Features. Please visit them here:

As you know, the more people show interest in a patch, the more likely it will go in quickly. Your help is appreciated!

Next time, I'll describe other components of the multilingual puzzle: taxonomy terms, static pages, etc.

AttachmentSize
81.31 KB
Author: 
Original Post: 

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web