Dynamic strings with placeholders

Last updated on
22 December 2016

The t() function family not only allows you to translate simple strings. It is possible that you need a variable component in the string: a name of a user, title of a post on the site, a dynamically generated link, and so on. So the t() function family, which includes format_plural(), allows you to use placeholders instead of passing in the dynamic string to them.

Take the time to think about it, understand these examples, and you'll never forget. If you pass on translatable strings with usernames in them, your translators would need to translate every single string with different usernames in them, so the number of variations for just this string could easily end up in the thousands. If you use placeholders, you reduce the number of strings to translate to one. Let's see some examples.

/**
 * Demonstrate the use of placeholders in t().
 *
 * While the example uses t(), this is true for st() and of course $t().
 */
function example_welcome_user_text($account = NULL) {
  global $user;
  if (!isset($account)) {
    $account = $user;
  }
  
  // DO NOT do this (1):
  // return t($account->name .', welcome to my website'); 

  // DO NOT do these either (2, 3, 4):
  // return $account->name . t(', welcome to my website');  
  // return $account->name .', '. t('welcome to my website'); 
  // return check_plain($account->name) .', '. t('welcome to my website'); 
  
  // This is right (5).
  return t('@username, welcome to my website', array('@username' => $account->name));
}

So why are these so many creative ways bad?

  1. t() works so if the translatable string is not found in the database, the provided string is saved for later translation. Consider that you can easily have hundreds or thousands users, this will save a translatable string for each user. Not good. There are also security (XSS) problems with this solution, read on.
  2. This clearly avoids the multiple variants saved in the database. But the translatable string starts with a comma, which will most probably make the translators puzzled, giving them no idea about the context of the string. They don't know whether there is a username or the phrase "Good morning", or whatever is before the comma. Not good. There are also security (XSS) problems with this solution, read on.
  3. While you might think moving the comma out solves the problem, it does not. The context even more missing from this example, since the translator will have no idea that anything appears before this string in the same sentence. Not good. Security problem still not resolved, read on.
  4. Solves the security problem at least. User provided data such as usernames, post titles, etc. might contain data which allow the submitter to perform XSS (Cross-site scripting) attacks. This is not a security book, so we are not going into the deep details here. The solution to that problem is that you need to escape the data. Assume that we are going to use this function in generating HTML, we should escape the output for HTML. So we selected check_plain(). That is great. Still, there can be languages where the username will not be in the beginning of the sentence, but somewhere in the middle or in the end. In this form it's impossible to position the username in the translations. That is not good.
  5. The t() function allows for an easier way. There are three types placeholders you can use designated by the special chars @, % and !. The documentation for FormattableMarkup::placeholderFormat() details the meaning of these markers. In this case, @ signifies a value to be sent through check_plain() before substitution. The second argument to t() provides the raw values for the substitutions, which are treated differently depending on which special marker you use.

This last example solves the security issue, while giving the translator context on where this string is used. Also, you'll only have one string to translate in your database. Isn't this great?

How you choose the name of placeholders is completely up to you, but it is good to keep some best practice. Try to make sure that your placeholder names are clear for translators. In the above example, using @n or @usern or @name or @user might not be as clear as @username. Unfortunately Drupal itself has some inconsistencies in how these placeholders are named (including the use of dashes or underscores) in placeholder names, so you should do better.

Let's do better than Drupal core

Let's see an actual example from Drupal 6 user module, from _user_mail_text().

t("!username,\n\nThank you for registering at !site. You may now log in to !login_uri using the following username and password:\n\nusername: !username\npassword: !password\n\nYou may also log in by clicking on this link or copying and pasting it in your browser:\n\n!login_url\n\nThis is a one-time login, so it can be used only once.\n\nAfter logging in, you will be redirected to !edit_uri so you can change your password.\n\n\n--  !site team", $variables, $langcode);

First, it can be made much more readable if you actually use newlines instead of the escape codes. Don't be afraid of including newlines in translatable text. This makes your code way easier to read and fix, and translators will see the same thing on their interface.

t("!username,

Thank you for registering at !site. You may now log in to !login_uri using the following username and password:

username: !username
password: !password

You may also log in by clicking on this link or copying and pasting it in your browser:

!login_url

This is a one-time login, so it can be used only once.

After logging in, you will be redirected to !edit_uri so you can change your password.


--  !site team", $variables, $langcode);

One good thing to note here is that emails are not sent in the HTML format, so none of the placeholders are replaced with escaped text. This is why they use the exclamation mark placeholder format. Keep in mind to choose the right format for the right output target!

Going forward from there though, there are a few problems. Drupal core in itself is not consistent in using underscores or hyphens in placeholder names. This email template uses underscores, but hyphens are more common and are the suggested form. Also, this email template is using _uri and _url suffixes, and if you look a bit closer, you'll see login_uri and login_url meaning two complete different things. The first one is the user login page for the site where you need to enter your name and password. The second one however is a one time login link, which includes a token only usable once. A better way this could have been done would be:

t("!username,

Thank you for registering at !site. You may now log in to !website-login-url using the following username and password:

username: !username
password: !password

You may also log in by clicking on this link or copying and pasting it in your browser:

!one-time-login-url

This is a one-time login, so it can be used only once.

After logging in, you will be redirected to !user-edit-url so you can change your password.


--  !site team", $variables, $langcode);

We have changed the three URL variables to say !website-login-url, !one-time-login-url and !user-edit-url. This is not only better for translators but also for website administrators, so they could find it much easier to update this template to their needs. Of course this would all require changes in the $variables array, so it is not possible to fix in Drupal 6 unfortunately. You should however strive to do better.