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:

  $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:

  $items[] = array(
    'path' => 'example/case1',
    '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:

  $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:

  t($menu_item['title'], $menu_item['title arguments']);

The D5 Case 2 equivalent would be:

  $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():

  $items['example/case3'] = array(
    'title' => t('Example title'),
    'title callback' => 'example_title_callback',
    'access callback' => 'example_access_callback',
    'page callback' => 'example_page_callback',
  );
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.
  $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',
  );
function example_title_callback($arg1, $arg2) {
  $title = t('!arg1 - !arg2', array('!arg1' => $arg1, '!arg2' => $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

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'),
  );
  $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;
}
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
}

Comments

NancyDru’s picture

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

Jaypan’s picture

Me too. Did you ever figure out a way around this? I've tried clearing the cache but it still takes the title and doesn't call my function.

Edit: Turns out I can't spell 'title'. For some reason I had decided it was spelled 'tile'. Fixing the spelling fixed the problem.

cpill’s picture

:D

jgreep’s picture

Please provide a link.

Jaypan’s picture

To what? This is the page anyone would link to.

jgreep’s picture

Sorry...A link to the t() function would give it a bit of polish.

martin_q’s picture

I've used your example for case #4 and the localisation is not happening. Whatever language was active when the menus were last rebuilt, this is the language used (where available) to localise the titles concerned. The current active language is completely ignored!

Instead I think the code needs to be as follows in order to make the localisation work - however this falls foul of the guidance explained in #561274: The first parameter to t() should be a literal string warning regarding the use of t() with non-literal strings. Difficult.

  $items['example/case4'] = array(
    'title' => 'Bike sheds full of blue smurfs', // COMPLETELY ignored. Good thing, too.
    'title callback' => 'example_title_callback',
    'title arguments' => array('Example title', 'Case 4'),
    'access callback' => 'example_access_callback',
    'page callback' => 'example_page_callback',
  );
function example_title_callback($arg1, $arg2) {
  $title = t($arg1) . ' - ' . t($arg2);
  return $title;
}

By the way, a use case for case #4 is where you might pass an argument from the URL (represented in the menu array key by '/%/') to the title_callback function in order to evaluate something, such as a taxonomy term name from its term id (tid). This would then not get translated by t() but by whatever method you have for localising user-defined strings. (You could probably do this using case #3 as well.)

Jaypan’s picture

You should change this:

$title = t($arg1) . ' - ' . t($arg2);

To this:

$title = t('!arg1 - !arg2', array('!arg1' => $arg1, '!arg2' => $arg2));

ranjeesh’s picture

I used the 'title callback' and 'title argument' to retrieve the node title from my module's database based on the page opened (using the parameter).

The following sample code might make it more clear.

//This is the menu item

$items['loadmap/%'] = array(
		'title' => t('Regional Offices'),
		'title callback' => '_get_page_title',
		'title arguments' => array(1), //This parameter is sent to the above callback function
		'page callback' => '_getcontent',
		'page arguments' => array(1),//Also am using the same parameter to retrieve page content
		'access arguments' => array('view map'),
		'type' => MENU_CALLBACK,
	);

Then adding the function to retrieve the page title


//This is the function which gets called by the menu item for page title
function _get_page_title($delta){

	$selectQuery = db_fetch_array(db_query("SELECT pagetitle FROM {xxx_page_title} WHERE mapid = '%s'", array($delta)));

	return $selectQuery['pagetitle'];
}

kalidasan’s picture

  $items['first/second/%'] = array(
    'title' => 'my title',
    'title callback' => '_call_back_title',
    'title arguments' => array(2),
  );

/**
 * Implements function to get dynamic title.
 */
function _call_back_title($arg) {
  return $arg. ' my title';
}
Jaypan’s picture

This:

function _call_back_title($arg) {
  return $arg. ' my title';
}

Should be this:

function _call_back_title($arg) {
  return t('@arg my title', array('@arg' => $arg));
}