Menu item title and description callbacks, localization

Last modified: January 19, 2009 - 13:10

Titles underwent several noteworthy changes in the Drupal 6.x menu system rewrite. While these changes add some crucial new flexibility to the menu system, the majority of contributed modules are unlikely to need most of the new features. In accordance with this fact, this page is organized with the most commonly-useful information at the top, and becomes more complex as you go along.

Of all the changes to the menu system in D6, there is one that ALL module developers should be aware of:

Titles and Descriptions should no longer be wrapped in t().

As of 6.x, Drupal internally handles the translation of menu titles and descriptions into the user's local language. Descriptions, if provided, are always translated with t(); there is no way to pass in additional data for placeholder substitution (in D5 and prior, passing in substitutions was a discouraged practice - with this change, the menu system enforces that rule directly). Titles are translated with t() by default, but t()-style string replacement is possible through the use of the new 'title arguments' property. You can also choose to replace t() with your own custom callback.

For most modules, all this really means is that the t() wrapping your Titles and Descriptions should be removed. However, the addition of the two new title-related menu properties ('title callback' and 'title arguments') means that there are now four possible ways of defining a title for each menu item. Note that in the following examples, the snippets all produce titles of the same form: the localized version of 'Example title - Case #', where # is the number of the case.

Case 1: Just 'title'

In Case 1, none of the new title-related menu properties are used. We simply strip out the calls to t() that should have been there in the D5 version:

<?php
  $items
['example/case1'] = array(
   
'title' => 'Example title - Case 1',
   
'description' => 'Description of the Case 1 item, with no t()!',
   
'access callback' => 'example_access_callback',
   
'page callback' => 'example_page_callback',
  );
?>

As a reference, here's what Case 1 would have looked like in Drupal 5:

<?php
  $items
[] = array(
   
'title' => t('Example title - Case 1'),
   
'description' => t("Description of the Case 1 item - but with t(), b/c this is D5."),
   
'access' => example_access_callback(),
   
'callback' => 'example_page_callback',
  );
?>

Again, most modules won't need anything more than Case 1.

Case 2: 'title' plus 'title arguments'

In Case 2 we're still letting the menu system use t() as the title callback function, but we're also passing some additional arguments to t() for substitution:

<?php
  $items
['example/case2'] = array(
   
'title' => 'Example !sub1 - Case !op2',
   
'title arguments' => array('!sub1' => 'title', '!op2' => '2'),
   
'access callback' => 'example_access_callback',
   
'page callback' => 'example_page_callback',
  );
?>

When the menu system comes across a link with this configuration - 'title arguments' and 'title' defined - then it calls t() like this:

<?php
  t
($menu_item['title'], $menu_item['title arguments']);
?>

The D5 Case 2 equivalent would be:

<?php
  $items
[] = array(
   
'title' => t('Example @sub1 - Case @op2', array('@sub1' => 'title', '@op2' => '2')),
   
'access' => example_access_callback(),
   
'callback' => 'example_page_callback',
  );
?>

Case 3: 'title' plus 'title callback'

In Case 3, we're changing the menu system's default behavior by having it call our own callback function instead of the default t():

<?php
  $items
['example/case3'] = array(
   
'title' => t('Example title'),
   
'title callback' => 'example_title_callback',
   
'access callback' => 'example_access_callback',
   
'page callback' => 'example_page_callback',
  );
?>

<?php
function example_title_callback($title) {
 
$title = $title . ' - ' . t('Case 3');
  return
$title;
}
?>

You'll notice that we run the output here through t() ourselves, and we do it in chunks - part of it even in the title, where we generally aren't supposed to do it anymore! When our title callback displaces t() as the localizer, we have to either call it ourselves in our custom title callback, or else do localization in some other way.

Please note that breaking up strings into small chunks like this is NOT an encouraged practice - it takes things out of context, which makes translation more difficult. It's only done here to illustrate this menu case.

Case 3 is the first case for which there is no clear D5 counterpart. There were ways to do things like this, but it meant loading additional data during hook_menu() and substituting it into the 'title' property right on the spot - messy, and potentially slowed down execution of hook_menu() for ALL modules, not just the one doing the loading (depending on how it was done).

Case 4: 'title callback' plus 'title arguments'

Case 4 is the most complicated of the four cases. In fact, it's a bit more of a "hack bolt on" to cover edge cases that the menu system designers hadn't yet thought of. With Case 4, there are two things to keep in mind:

  1. When you define both a 'title callback' and 'title arguments', the menu system will completely ignore anything that's put into the 'title' property.
  2. The menu system has to use call_user_func_array() to accommodate this case. Whereas t() expects a second argument that is an array (and so can take the contents of 'title arguments' directly), there is no guarantee that the callback defined in 'title callback' can handle an array for a second argument.

<?php
  $items
['example/case4'] = array(
   
'title' => 'Bike sheds full of blue smurfs', // COMPLETELY ignored. Good thing, too.
   
'title callback' => 'example_title_callback',
   
'title arguments' => array(t('Example title'), t('Case 4')),
   
'access callback' => 'example_access_callback',
   
'page callback' => 'example_page_callback',
  );
?>

<?php
function example_title_callback($arg1, $arg2) {
 
$title = $arg1 . ' - ' . $arg2;
  return
$title;
}
?>

Nothing about blue smurfs or bike sheds will ever make it into the example_title_callback() function - ONLY the data set as the 'title arguments'. Also, as with Case 3, our title callback function needs to take care of localization since we've superseded the menu system's internal call to t().

Some Examples from D6 core

<?php
function block_menu() {
 
$items['admin/build/block'] = array(
   
// Title as literal string, callback not defined, so falls back to the default t() callback
   
'title' => 'Blocks',
   
// Description as literal string, always translated with t().
   
'description' => 'Configure what block content appears in your site\'s sidebars and other regions.',
   
'page callback' => 'drupal_get_form',
   
'page arguments' => array('block_admin_display'),
   
'access arguments' => array('administer blocks'),
  );
?>

<?php
  $default
= variable_get('theme_default', 'garland');
  foreach (
list_themes() as $key => $theme) {
   
$items['admin/build/block/list/'. $key] = array(
     
// Title is string with placeholder, callback not defined, so falls back to the default t()
     
'title' => '!key settings',
     
// "title arguments" specify the arguments to pass on to the title callback
     
'title arguments' => array('!key' => $theme->info['name']),
     
'page arguments' => array('block_admin_display', $key),
     
'type' => $key == $default ? MENU_DEFAULT_LOCAL_TASK : MENU_LOCAL_TASK,
     
'weight' => $key == $default ? -10 : 0,
    );
  }
  return
$items;
}
?>

<?php
function search_menu() {
 
//...

 
foreach (module_implements('search') as $name) {
   
$items['search/'. $name .'/%'] = array(
     
// Custom callback to get the title from
     
'title callback' => 'module_invoke',
     
// List of arguments to pass to "title callback"
     
'title arguments' => array($name, 'search', 'name', TRUE),
     
'page callback' => 'search_view',
     
'page arguments' => array($name),
     
'access callback' => '_search_menu',
     
'access arguments' => array($name),
     
'type' => $name == 'node' ? MENU_DEFAULT_LOCAL_TASK : MENU_LOCAL_TASK,
     
'parent' => 'search',
    );
  }
  return
$items
}
?>

Oops

NancyDru - February 11, 2009 - 16:40

Under Case 4, item #1 - that appears to not be the case. I'm seeing the user module Title string being used regardless of the callback.

NancyDru

much easier than using t()

cpill - March 2, 2009 - 12:36

:D

 
 

Drupal is a registered trademark of Dries Buytaert.