Making your custom data translatable

Last modified: September 23, 2009 - 16:36

When developing a module, you should give some consideration to making the text your module displays translatable into other languages. An overview of this process is given on the Multilingual Support page.

This page concerns one step in that process: making user-entered text in your module translatable. Note that if all of the user-entered text in your module is stored in nodes, probably the core Locale and Content Translation module will take care of translations for you. But if your module lets the user enter data other than nodes, and that data will be displayed to users, then continue reading to find out how to make it translatable.

The core Locale module supports enabling of languages, designation of a default language, and import/export of language files (.pot, .po) to be used and edited with external software. See its handbook page: http://drupal.org/handbook/modules/locale.

As of Drupal 6, the locale module enables multiple types of data to be translated--not just code-based strings. Drupal core includes support for a single type: strings defined at the code level in e.g., .module files. But other modules can register their own types by implementing hook_locale().

The contrib Internationalization package (i18n) makes extensive use of hook_locale() to make various types of Drupal core data translatable: taxonomies, variables, forums, user profiles, etc. i18n also includes a generic multilingual strings module, which provides a toolset that module authors can use to expose data through the locale system.

The locale system is best suited to data that change relatively infrequently and less suited to longer, more complex strings that may need frequent updating.

A key characteristic of the locale system is that there is only one version of a given item. String substitution happens at run time, e.g., before display. It's distinct in this way from the content translation system, in which there are multiple versions of a given piece of content, one per language.

Module developers wanting to make their data translatable via the locale system have several options including:

  1. Work directly with the locale system
  2. Build on the Internationalization - String Translation module and use its API
  3. Introduce custom handling with multiple versions of a given item, one per language.

Each of these options is described in a section below.

Some handy methods

Methods useful when working with locale and languages include:

Working directly with the locale system

To register one or more new "groups" (types of strings) to be recognized by the Locale module, you need to implement hook_locale(). However, the Locale module doesn't provide you much help beyond this hook.

The locale system is still focused mainly on the "default" case (strings passed through t(), generally in code). The locale() function is hard-coded to this use case. While studying the locale() function might be useful, it won't directly help you to work with your own custom locale group. The locale system provides a UI for translating strings once they're registered, but, when it comes to registering strings and ensuring they're used when needed, you're on your own. Hence in part the creation of the strings module in i18n to facilitate these tasks.

To work directly with the locale system, you need to:

  • Save source strings as they are created to the {locales_source} table.
  • Update existing translations when they change.
  • Load strings if available from the {locales_target} table for the current language.

Working with the Internationalization - String Translation module

The basic process for using the String Translation API from the Internationalization module for translating your strings is as follows:

  1. Implement hook_locale() to define a translation group for your module; for example, you might choose 'terms_of_use' for the Terms of Use module. See below for completed sample implementation.
  2. Make sure that you only invoke the Strings Translation module if it has been installed, by wrapping all of the calls into its functionality in:
    if (module_exists('i18nstrings')) {
    }
  3. Locate places in your module where a user can edit a text string that is then displayed on the user interface. Make sure you tell them to enter the text in their site's default language, and perhaps give them some direction on how to translate this text using the Internationalization - Strings Translation module.
  4. Assign each of these strings a behind-the-scenes code name, consisting of your module's translation group name (defined in your hook_locale() implementation), a subgroup code for the section within your module (if desired), and a code specific to the string, separated by colons (e.g. 'terms_of_use:tou:checkbox_label' could be used as the code name for the label the user enters for a checkbox in the Terms of Use module on the 'tou' screen).
  5. Whenever the user interface text is updated by a user, call the tt() function to store the new value for that text. For example:
    // First store the text in variable_set() or a database table. Then:
    if (module_exists('i18nstrings')) {
      $language = language_default('language');
      tt('terms_of_use:tou:checkbox_label', $text_user_entered, $language, TRUE);
    }
  6. Whenever you need to display the user interface text, call the tt() function to retrieve the current translation of that text. For example:
    // First retrieve the text from variable_get or a database table. Then:
    if (module_exists('i18nstrings')) {
       $text_to_display = tt('terms_of_use:tou:checkbox_label', $text_to_display);
    }
  7. Add a 'refresh' operation to your hook_locale() implementation, which retrieves the untranslated text from the database and calls the tt() function on it. Sample hook_locale() implementation:
    function terms_of_use_locale($op = 'groups', $group = NULL) {
      switch ($op) {
        case 'groups':
          return array('terms_of_use' => t('Terms of Use'));
        case 'refresh':
            if(module_exists('i18nstrings')) {
              // Get the checkbox label from the database or from variable_get first. Then:
              tt('terms_of_use:tou:checkbox_label', $label_text, NULL, TRUE);
            }
      }
    }

For further reading: these two issues are useful references on how other contributed modules added support for translation using the String Translation module:

You might also study the code in the Internationalization module itself, as much of its module set is built off of its String Translation module.

Providing custom multilanguage support

Custom handling may be needed where strings are long, complex, or frequently updated.

  1. Introduce a language field in your schema.
  2. On the editing form, present a language selection element. See locale_form_alter() for examples.
  3. On insert and update, save the language field along with other data.
  4. On load, load a version of your data in the appropriate language, if present. (Use the global $language variable.)
  5. On view, provide e.g. a 'translate' link, loading a form with a copy of the item ready for translation.

Wrapper function

Kars-T - November 2, 2009 - 14:49

Funny that from all there is you took my code as example. Thank you! :D

In thread #471546: Translate relationships (as 'friend') commet #15 from the fine user_relationships module we figured that a wrapper function would be a nicer solution:

<?php
/**
* Wrapper function for tt() if i18nstrings enabled.
*/
function ur_tt($name, $string, $langcode = NULL, $update = FALSE) {
  if (
module_exists('i18nstrings')) {
    return
tt($name, $string, $langcode, $update);
  }
  else {
    return
$string;
  }
}
?>

All above in the article is a correct solution and should be done that way except all those if (module_exists('i18nstrings')) { calls. A wrapper function like this leads to more centralized and smaller code.

Just to mention: module_exists uses a static variable to cache results so this is no speed issue.

--
Best regards

Kars-T

Separate module

danielb - November 8, 2009 - 00:52

Another solution I'm looking into, if you have a more complicated module that lets users create some kind of object that needs to be translatable, is to create a separate module that depends on i18nstrings and the module in question. You just need hooks in the original module to be able to alter the object before viewing it, and upon saving it - much like hook_nodeapi.
My strategy for gathering the strings is to programatically load the form array for the edit page of the object (using code out of drupal_get_form), iterate through the fields and decide which ones should be translatable - usually the non-empty non-numeric textfield and textareas, then save/create those strings with tt using the values from the object. I also make allowances for my decisions to be drupal_altered and the form itself can specify a #translatable attribute on elements to force translation to be true or false.
Then when you are on the view page of your object you iterate through the object and make the correponding replacements with tt.

discussion ongoing at

aufumy - November 17, 2009 - 00:07

discussion ongoing at http://drupal.org/node/606804, related to making modr8 module translatable.

Another $op for hook_locale()

Boobaa - November 21, 2009 - 19:30

When I was struggling with #128228: Make case states translatable, I just found another $op for hook_locale():

<?php
/**
* Implementation of hook_locale().
*/
function casetracker_locale($op = 'groups', $group = NULL) {
  switch (
$op) {
    case
'groups':
      return array(
'casetracker' => t('Case Tracker'));
    case
'refresh':
      if (
module_exists('i18nstrings')) {
       
casetracker_locale_refresh();
      }
      break;
    case
'info':
     
$info['casetracker']['refresh callback'] = 'casetracker_locale_refresh';
      return
$info;
  }
}
?>

This $op = 'info' is being used by i18nstrings_admin_refresh() when building the radios part for ?q=admin/build/translate/refresh. The first-level key of the returned $info array should be the same in $op = 'groups' to be able to identify which group of locale strings should be refreshed.

 
 

Drupal is a registered trademark of Dries Buytaert.