Translating user-entered strings in D7

By:

on

May 21, 2014

The Internationalization suite of modules do a pretty good job of making everything translatable in Drupal 7. But, if you're writing a module that stores its data outside variables or entities, you might notice a few gaps.

On a recent project, I noticed that field display settings (field formatter settings for those familiar with the Field API) are not translatable by default. In many cases, this is fine because the field settings in core modules don't make sense to translate: for example, the number of characters to trim a summary at or the image style to use.

Never use t($user_entered_string)!

It's tempting to solve this problem by passing the variable through t() as you're outputting it (e.g.: $label = t($settings['label'])), however doing so is a bad idea!

Why t($variable) is usually a bad idea

To quote the documentation for the t() function,

You should never use t() to translate variables, such as calling t($text);.

There are a few reasons for this:

  • In addition to actually translating text at run-time, the t() function is also used by text-extraction routines to find text that needs to be translated, and build databases of text to be translated for translation teams.
    Both the Internationalization's built-in string translator and the Translation template extractor module (which powers the Drupal Translations site) ignore calls to t(), whose first argument contains any variable (e.g.: t($var), t($var . 'text'), etc.): these won't even show up as being available to translate!
  • The t() system doesn't support updates to existing strings. When user data is updated, the next time it's passed through t(), a new record is created instead of an update. The database bloats over time and any existing translations are orphaned with each update.
  • The t() system assumes any data it receives is in English. User data may be in another language, producing translation errors.

The only case in which variables can be passed safely through t() is when the string in the variable will be passed through t() elsewhere.

Why t($user_entered_string) is a security risk

To quote the t() function documentation again,

It is especially important never to call t($user_text);, where $user_text is some text that a user entered - doing that can lead to cross-site scripting and other security problems.

For example, a malicious translator could enter HTML in their translations (a cross-site scripting (XSS) attack).

Introducing i18n_string()

So, what is a module developer to do? As it turns out, the Internationalization project's String translation sub-module (i18n_string) has a function named i18n_string(), which makes it easy to store and retrieve translations of user-entered strings with only minimal changes to your module.

Remembering your keys

To ensure strings get updated, not orphaned, i18n_string() requires a $key parameter that uniquely identifies that instance of the string to store and load the string.

For example, if the string is always displayed the same way, a simple key will suffice (like a variable name). However, if the string has variants, like a block title (each block could have a different title) or a field formatter setting (each content type could have different settings for each of its display modes; and there could be more than one field of that type in a content type), you'll want to construct the key from the identifiers that make that variant unique (e.g.: blocks are uniquely identified by module and delta; field formatter settings are uniquely identified by entity, bundle, view mode and field name).

Registering and translating strings

The i18n_string() function has two operations: registering a user-entered string when it's saved (this is referred to as updating the string in the API) and translating (retrieving) the string when it's displayed.

To register a string, call: i18n_string($key, $value, array('update' => TRUE)); where $value is whatever the user entered. Note you should still store $value somewhere so you can provide a default if the user doesn't have i18n installed (more about this later).

Note that it's really easy to insert this call into your existing code: you don't have to change the way your module already stores / retrieves the setting.

To translate a string, call: $string_to_display = i18n_string($key, $value); where $value is whatever the user saved earlier. As with any user-entered string, make sure you've check_plain()ed or filter_xss()ed it

Again, note that it's really easy to insert this call into your existing code.

Best practices

Currently, the i18n_string() function isn't part of D7 core (although it exists in the form of translatable configuration in D8 core), so you have to ensure i18n_string() is available before you call it, otherwise you'll get "Call to undefined function" errors.

The documentation on using the i18n API from other modules suggests wrapping calls to it in functions which check if the function has been defined and calls it if it does (or acts as if nothing happened if it doesn't exist):


function _mymodule_translate($name, $string, $langcode = NULL) { return function_exists('i18n_string') ? i18n_string($name, $string, array('langcode' => $langcode)) : $string; } 
 
function _mymodule_translate_update($name, $string, $langcode = NULL) { return function_exists('i18n_string') ? i18n_string($name, $string, array('update' => TRUE, 'langcode' => $langcode)) : FALSE; } 

Note that the translate function above returns its $string parameter if i18n_string() isn't available.

Conclusion

The i18n_string() function makes it easy to bolt translation on to existing code without changing the way your module works.

If you want to see an example of how I solved the field display setting translation problem for the Entity registration module, you can see my patch at #2234915: Registration link text (field formatter label) is not translatable.

Add new comment

Plain text

  • No HTML tags allowed.
  • Lines and paragraphs break automatically.