5. Lost in Translations – Drupal 6 JavaScript and jQuery

Chapter 5. Lost in Translations

Drupal offers some enticing JavaScript tools, one of which is jQuery. The theming and behavior capabilities provided by drupal.js are other examples. Along with those cool tools comes a feature that has had a remarkable influence on the success of Drupal, but which provides far less glitz and glamour.

This tragic hero is the translation engine, which will be the subject of this chapter.

Translations are importantone might even say vitalto the success of Drupal. Consequently, it is imperative that all Drupal developers become familiar with these tools. JavaScript written in Drupal 6 (and in later versions) should be translation-aware.

Note

Even if you don't think you need the translation functions, I advise you to read this chapter. The tools covered here play a very important role in Drupal, even providing additional security to your code.

We will move quickly in this chapter, retaining our focus on the practical. We will not spend time in closely examining the translation system.

Here are the things we will cover in this chapter:

  • Get our bearings in the drupal.js library

  • Enable multi-language capabilities in Drupal

  • Learn the translation functions

  • Build language files

For our project, we will create a small tool that takes advantage of JavaScript translation features. And to see it in action, we will create our own translation.

Translations and drupal.js

There are four main families of tools in drupal.js:

  1. Theming functions.

  2. Translation functions.

  3. Utility functions.

  4. Support for Drupal behaviors.

Our focus in this chapter will be on the translation functions. When we talk about translation tools, what exactly are we talking about?

Translation functions provide language translation facilities to JavaScript. Text that would normally be hardcoded into the JavaScript is translated through this system to the user's preferred language.

As is the case with the theming system, the drupal.js translation system is designed to provide an API similar to the server-side PHP translation system.

The translation functions are designed to be simple for the developer's use. In fact, the developer needn't even turn on Drupal's translation module to use the JavaScript libraries. The idea is to make it painless enough for the developer to use, and train the developer to habitually use the translation features.

In order to show how things work, we will not only look at the translation functions, but also at how the larger translation system is used.

Translation and languages

One of the Drupal's more distinguished points is its well-integrated support for multiple languages. Drupal has been translated into dozens of languages, and installing and enabling a translation is a simple process. For these reasons, Drupal has gained an international audience.

In earlier versions of Drupal, this language support was confined to server-side PHP code. JavaScript did not have access to the translation library. But with the release of Drupal 6, basic translation support was extended to JavaScript.

In order to see how translations work, we are going to walk through the process of enabling the translation system on the server. We will then return to the drupal.js library to see how it uses the system.

Note

Translation functions are the portions of code that developers use to make it possible for code to perform translations when appropriate. The translation system is the part of Drupal that does the actual translation. We will start with this second part, the translation system, and then go back to the translation functions.

English is the default language for Drupal. In fact, it is the only one installed by default. But since Drupal provides a complete language translation subsystem, and Drupal code is developed to support translation, enabling multi-language support is a straightforward process.

We will begin by installing a new language.

There are three steps that must be performed the first time you install a language:

  1. Multi-language support must be turned on.

  2. Translation files must be downloaded and installed.

  3. Drupal's translation preferences must be configured.

We will briefly walk through this process.

Turning on translation support

By default, Drupal's translation support is disabled. It is disabled for the practical reason that if it is not needed, the performance hit incurred by the translation subsystem should be avoided.

Turning it on is a matter of enabling a couple of modules. These modules are included in the Drupal core, so there's no need to download anything. All you need to do is go to Administer | Site building | Modules, and then check the boxes next to the Locale and Content translation modules.

Once you've done that, click on the Save configuration button at the bottom of the screen. That should do it.

Getting and installing translations

Dozens of translations are available in the Translations repository on the official Drupal.org web site. To find and download a new language, go to http://drupal.org/project/Translations and download the desired language.

Once you have the translation archive, you can install it by uncompressing the file in the same directory where Drupal is installed. For example, if Drupal is installed in /var/www/drupal (a common location for it on Linux servers), you will want to uncompress the translation file in /var/www/drupal. The language files will automatically be placed in the correct location.

The next thing to do is to let Drupal know that you have a new language installed.

Configuring languages

Once we have downloaded and unpacked the desired language(s), we need to configure Drupal's language support to determine how to handle multiple languages.

There are two steps to this process:

  1. Add the new language.

  2. Configure the global language settings.

In the first step, we are going to let Drupal know about the new language.

Adding the language

We've already installed the language, but we also need to tell Drupal that we want it to go through the process of scanning the language files and compiling a translation database. This process is called adding a language.

To do this, we need to go to the Administer | Site configuration | Languages page and click on the Add language tab as seen in the following screenshot:

On this screen you will need to select the language from the Language name drop-down list. Unfortunately, this list is not limited to the languages you have already installed, so you will have to find the language in the list. Languages are indexed by their English name. Thus, you should look for German instead of Deutsch.

Once you've found the language, click Add language and sit back while Drupal parses all of the language files.

After the parsing is finished, we are ready to move on to the next step.

Configuring languages

We have multiple languages supported, now. But we need to tell Drupal how it should determine what language we want to see when we visit a page.

To configure this, we can click on the Configure tab on the Administer | Site configuration | Languages page. There is only one set of options on this page: Language negotiation.

These settings let us configure how Drupal will determine which language to display. By default, None is checked. This means only the default language will be used.

Path prefix only determines which language to use based on a language identifier string present at the beginning of the URL. For example, my site is running athttp://localhost:8888/drupal/. I have English set as the default language, and the Spanish translation is also installed.

Using these settings if I type in the previous URL, I will see the page in English (the default language). However, if I type in the URL http://localhost:8888/drupal/es/, the site will be displayed in Spanish. The es identifier is a prefix to the Drupal portion of the URL. So if I want to view a node using the Spanish translation, the URL would look like this: http://localhost:8888/drupal/es/node/1.

Note

Path translation and language prefixes

The URLs mentioned make use of Drupal's clean URLs. By using Apache's mod_rewrite module, data that would normally appear in a query string can be embedded in the URL. If you do not have clean URLs turned on, then the previous URL would look something like this: http://localhost:8888/drupal?q=es/node/1. With the query string clearly isolated, it's a little easier to see how es is treated as a prefix.

The Path prefix with language fallback option is similar to the previous option, except that it adds one more step.

If the path provides a language prefix, then that language is used (assuming the language has been installed and added). But if no prefix is found, Drupal then checks the language preferences that the web browser sends in its HTTP headers. These look something like this:

User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.0.1) Gecko/2008070206 Firefox/3.0.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: es,en-us;q=0.7,en;q=0.3

Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Cache-Control: max-age=0

This is a subset of the HTTP headers my browser sent when requesting a page from Drupal (and I viewed the headers using Firebug).

The highlighted line shows the language preferences. Spanish (es) is the first language, with US English (en-us) and generic English (en) set as my second and third choices.

With Path prefix with language fallback enabled, when I type in http://localhost:8888/drupal/, I will get the page in Spanish because Drupal will inspect the Accept-language header and determine that it is the best language to use.

If the Accept-language header isn't available, or there is no language match, then Drupal will fall back to the site's default language.

Finally, the last language negotiation type is Domain name only. In this case, the domain name portion of the URL is used to determine language. For example, http://es.example.com would resolve to the Spanish language, while http://en.example.com would resolve to English.

For multi-language development work, I find the Path prefix only choice to be the easiest to work with.

Note

The translation feature is used to translate the strings that appear in Drupal code. This is done manually by a dedicated team of translators. Consequently, enabling translation will not affect the content you create. For example, if you write content in English, it will not be translated to Spanish for you. Only the interface (built-in menus, module descriptions, and so on) will be translated.

We now have multi-language support enabled, and you should be able to configure your Drupal installation to use more than one language. It's time to take the developer's perspective again. First, we will look at the main JavaScript translation functions. Then, we will look at a developer's tool to create translations.

Using the translation functions

Regardless of whether or not you intend to translate your module, you should always use the translation functions where applicable. There are a few reasons for this:

  • By coding in a translation-friendly way, you pave the way for easy translations later. This is especially important for contributed modules, where your module may indeed be used by speakers of other languages.

  • The translation functions provide additional security. This might sound counterintuitive at first. How can adding translation support increase security? As we will see shortly, the translation functions also perform additional escaping on text. Untrusted text is automatically escaped for display. Escaping is one way of preventing a malicious user from performing Cross-Site Scripting (often called XSS) scripting attacks or other code injection attacks.

  • Using translation functions is just good coding practice. As with many other aspects of Drupal coding, the developer community encourages (and in many cases enforces) clean, well-written, and portable code. Using the translation functions is one way of conforming to Drupal's coding guidelines.

The drupal.js file contains a pair of functions that can make use of Drupal's multi-language support. These two functions are Drupal.t() and Drupal.formatPlural().

If you've done any Drupal PHP coding, both of these should immediately be familiar to you. They are directly analogous to the t() and formatPlural() functions in Drupal's core PHP library. Not only do they share a name, but also the same method signature. They take the same arguments and return the same type of content.

Let's start out by looking at the Drupal.t() function.

Note

In the previous chapter, we looked at the jQuery.extend() function, and I mentioned that it worked like a static function. Many of the functions we will see in the drupal.js library are also used this way. There is no need to call new Drupal(). In fact, the Drupal object has no constructor, so it cannot be used to create new instances.

The Drupal.t() function

As with all of core Drupal JavaScript functions, this function uses the Drupal namespace. The t() function is a member of the Drupal library. This function's job is to take a string and perform any translation actions on it. Here's a simple example of this:

alert(Drupal.t('hello world'));

In this case, the translation function would check the language database for the user's preferred language and see if there was a translation available. If there is, then the function will return the translated string. If not, then hello world will be returned unaltered.

Shortly, we will take a closer look at how the translation happens. It is a slightly more complex process than what initially meets the eye. But before we move on in that direction, let's look at a more complex use of the Drupal.t() function.

The Drupal.t() function can take up to two arguments. They are (in order):

  1. The string that should be translated.

  2. An object containing name/value pairs for substitution into the string.

Here's a brief example that makes use of both:

var params = {
"@siteName": "Example.Com",
"!url": "http://example.com/"
};
var txt = Drupal.t("The URL for @siteName is !url.", params);

In the code, we first create the params object that contains a mapping of placeholders to text. What is this mapping for? Look ahead to the contents of the Drupal.t() function. The Drupal.t() function takes a string object and the params mapping we created.

The string looks like this: The URL for @siteName is !url. There are two placeholders in this string, @siteName and !url. When the Drupal.t() function is executed, the placeholders will be replaced by values from the params object.

In this case, @siteName will be replaced by Example.Com, and !url will be replaced by http://example.com/. So the English rendering of the string would be The URL for Example.Com is http://example.com/.

But wait! There are a couple of details to fill in. First of all, why are we using placeholders in the first place? And second, what are the @ and ! signs for?

In answer to the first question, placeholders should be used for any values that should not be translated. The example uses a proper name for @siteName and a URL for !url. In cases like this, translation would be unnecessary. Presumably, the site name and URL are the same in all languages.

This is a simple case where placeholders might be used. However, it's not all that common in practical cases.

A more realistic use of placeholders is to substitute it in values that are not known at translation time. To elaborate the example, consider the case where the site name and site URL are retrieved from some other object. Let's say we have an object called SiteInfo that contains this information (This is a fictional example. There is no such object.)

Our params object might look like this instead:

var params = {
"@siteName": SiteInfo.name,
"!url": SiteInfo.url
};

Here, the values of these variables may not be known until runtime, long after the translation has been generated. So using placeholders clearly makes sense.

Note

Translations are created by humans, and the process of translation is mostly handled manually. We will see this process in a few minutes. But nothing magical happens at runtime. Translated strings are simply substituted for the default (usually English-language) text.

Placeholders are then used in cases where values need to be inserted into a translated string, but where the values themselves should not be translated as part of that string.

In answer to the second question, placeholders can be demarcated by three different symbols: @, %, and !. Any word (alphanumeric characters surrounded by whitespace) inside a translation string that begins with one of those three characters will be treated as a placeholder.

Each of these three placeholder symbols serves a special purpose. Each indicates to Drupal.t() how the string should be substituted in, as explained here:

  • Placeholders that begin with the @ symbol are escaped for display in HTML. For example, if we have a param that looks like this:'@tag': '<p>', Drupal.t() will convert the value to&lt;p&gt; before substituting it into the target string. Mostly, you should use this method of escaping to prevent security holes.

  • Placeholders that begin with ! are inserted verbatim. Drupal does not encode any of these. This should be used with care, for it could open security holes that might, for instance, allow XSS attacks.

  • Finally, placeholders that begin with % are first encoded (like @ placeholders), and then themed for emphasis. It means, in the default Drupal configuration, the resulting string will be placed inside the<em></em> tags. Using the example'@tag': '<p>', the output would be<em>&lt;p&gt;</em>.

So what should you use and when? Most of the time, placeholders should be prefixed with @. That will do the encoding, but without necessarily adding any additional format (like % does). Placeholders should only begin with ! when escaping content would damage the output, and when the value to be substituted is known. For example, you shouldn't take user-entered text and then use a ! placeholder.

That's how the Drupal.t() function works.

When should a string be translated?

Ideally, every static piece of text in your applicationlabels, help text, descriptions, and so onshould be translated. Of course, there are exceptions. For example, proper nouns are usually not translated.

The Drupal.formatPlural() function

The second translation function is Drupal.formatPlural(). As you may have guessed from the name of the function, its job is to format a reference to singular and plural objects. This comes from the problem that in many languages (English and Spanish are good examples) single items and plural items have different suffixes. For example, we say "Johnny has 1 apple" and "Johnny has 2 apples". We also say, "Johnny has 0 apples."

So 1 is the only singular case in English (not all languages are this way, French treats 0 as singular). To handle this in a translation-friendly way (not all languages add s to form a plural), Drupal contains a function Drupal.formatPlural() that can determine whether the current case needs a singular form or a plural form.

This function takes these arguments:

  • A number (If it is 1, then the singular will be used, otherwise the plural form will be used.)

  • A singular string (in English)

  • A plural string (in English)

Elaborating our example, we might have code that looks something like this:

for (i = 0; i < 6; ++i) {
alert(
Drupal.formatPlural(i, "Johnny has 1 apple.", "Johnny has
@count apples.")

);
}

The formatting is a little stilted to get everything on one line, but the important part is the highlighted call to Drupal.formatPlural().

When this script is run, it will loop six times and pop-up an alert message each time. Each time Drupal.formatPlural() is called, it will be passed i and the singular and plural strings.

If the value of i is 1 then the alert will say Johnny has 1 apple. In all other cases, the third parameter will be used: Johnny has @count apples. The @count placeholder is automatically replaced with the value of i. So for the first loop, we get Johnny has 0 apples. On the third loop, we get Johnny has 2 apples.

But this function doesn't just toggle between two strings. It uses the translation subsystem to translate the selected string too. So if the language is set to German and i is 0, the output should look something like this (assuming the German translation exists): Johnny hast 0 Äpfel.

That's all there is to the Drupal.formatPlural() function. The next thing we will be look at is how to translate a string and make it available to your JavaScript.

Adding a translated string

When we create a translation for our content, we want to fulfill two goals:

  1. Build a translation in such a way that the Drupal.t() function can make use of it.

  2. Make this translation portable, so that we can use the same JavaScript on different servers. Even if we are only planning on using our JavaScript on a single site, we want it to be portable for ease of migration or rebuilding.

The easiest way to meet these two goals is to install a special module. This module is called the Translation template extractor. It basically analyzes our code, looking for the Drupal.t() calls. It then generates a template that we can easily modify to add our translation.

To get this module, go to http://drupal.org/project/potx and get the latest release. The release contains both a module and a command-line tool. If you like, you can use the command-line version. However, the module version is very easy to use. It is installed simply by moving the potx/ folder in the downloaded module to your sites/all/modules directory, and then installing the module in the usual way by visiting Administer | Site building | Modules.

The main thing this module does is add a new tab to the Administer | Site building | Translate interface page:

The Extract tab (on the far right of the list of tabs) is the one that we use to parse our files and get the strings for translation. In just a little while, we will use this interface to grab the contents from a JavaScript project that we will be creating.

This interface will generate a special file called a POT file, which maps the original untranslated text to translated strings.

Note

Drupal uses the GNU gettext system for translation. Learn more about it at http://www.gnu.org/software/gettext/.

Once you've gone through the process of translating strings in this POT file, it is just a matter of putting the translation file in the right place in the theme (or module) directory. Again, we will walk through that in our project.

Note

Translating JavaScript, translating PHP

We are focused on the JavaScript here. However, PHP translations are done in exactly the same way. There's no need to learn two translation systemsthe two are fully integrated.

In fact, now that we have gone over the basics, we are ready to start our project.

Project: weekend countdown

The project that we will create in this chapter is a simple weekend countdown tool. This will display a little piece of text that indicates the current day of the week, and then says how many days are left until the weekend.

The main point of this application will be to make practical use of the translation system that we saw earlier. For that reason, we will first write some code, and then do a little translation.

Note

While we will consistently use the Drupal.t() function in this book, this is the only place where we will be writing a translation. You do not need to provide translations along with your theme or module (though if you have the ability to do the translations, it sure would be nice).

Our code is once again going to be attached to the frobnitz theme. The script file will be named day.js. Make sure you include it in the frobnitz.info file: scripts[] = day.js.

Here's the code:

var Day = Day || {};
Day.dayNames = [
Drupal.t("Sunday"),
Drupal.t("Monday"),
Drupal.t("Tuesday"),
Drupal.t("Wednesday"),
Drupal.t("Thursday"),
Drupal.t("Friday"),
Drupal.t("Saturday")
];
/**
* Create a small banner indicating the number of days until
* the weekend.
*
* This will create a div element in the upper right-hand
* corner.
*/
Day.banner = function () {
var divProps = {
"position": "absolute",
"top": "5px",
"right": "25px",
"background-color": "black",
"color": "white",
"padding": "4px"
};
var today = (new Date()).getDay();
var dayCount = 6 - today;
var dayFields = {
"@day": Day.dayNames[today],
"@satCount": Drupal.formatPlural(dayCount,"is 1 day", "are @count
days"),
"@saturday": Day.dayNames[6]
};
var dayText = Drupal.t(
"Today is @day. There @satCount until @saturday.", dayFields);
var dayDiv = '<div id="day_div"></div>';
$('body').append(dayDiv).children('#day_div').css(divProps)
.text(dayText);
};
$(document).ready(Day.banner);

The main thing this code does is create a box in the upper-right corner of the screen that looks like this:

With this in mind, let's step through the code by beginning with the top portion:

var Day = Day || {};
Day.dayNames = [
Drupal.t("Sunday"),
Drupal.t("Monday"),
Drupal.t("Tuesday"),
Drupal.t("Wednesday"),
Drupal.t("Thursday"),
Drupal.t("Friday"),
Drupal.t("Saturday")
];

After creating a Day namespace object, we create an array with seven entries, one for each day of the week. The value of the entry will be the result of a call to Drupal.t(). The resulting English-language version would be something like this: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].

Later we will use this array to match the numeric index of the weekday to the name of the day. This is done in the Day.banner() function seen here:

Day.banner = function () {
var divProps = {
"position": "absolute",
"top": "5px",
"right": "25px",
"background-color": "black",
"color": "white",
"padding": "4px"
};
var today = (new Date()).getDay();
var dayCount = 6 - today;
var dayFields = {
"@day": Day.dayNames[today],
"@satCount": Drupal.formatPlural(dayCount, "is 1 day", "are
@count days"),
"@saturday": Day.dayNames[6]
};
var dayText = Drupal.t( "Today is @day. There @satCount until
@saturday.", dayFields);
var dayDiv = '<div id="day_div"></div>';
$('body').append(dayDiv).children('#day_div').css(divProps)
.text(dayText);
};

The first thing we do in this function is create divProps, which serves as a map of all of the CSS properties that we will assign later to our<div></div> element.

After that, we find the numeric value of the current day of the week and then calculate how many days are left until Saturday:

var today = (new Date()).getDay();
var dayCount = 6 - today;

Both of these values are stored for later use.

In order to create compact code, the previous code uses a shortcut. The code (new Date()).getDay() creates an anonymous instance of the Date prototype and then calls that object's getDay() function. The function getDay() returns the numeric index of the current day, where Sunday is 0 and Saturday is 6.

This is effectively the same as writing:

var myDate = Date();
var today = myDate.getDay();

Our shortcut method is useful only in the case where the new Date instance is needed only once. By using it, we spare ourselves a line of code and an extra variable. However, there's nothing wrong with using the two-line version.

Now that we have the current date and the number of days until Saturday, we can continue.

var dayFields = {
"@day": Day.dayNames[today],
"@satCount": Drupal.formatPlural(dayCount, "is 1 day", "are
@count days"),
"@saturday": Day.dayNames[6]
};
var dayText = Drupal.t("Today is @day. There @satCount until
@saturday.", dayFields);

The pattern of the previous code should look familiar because it is the setup for a Drupal.t() call.

First, we define our placeholders. This call is a little packed, so let's look at it closely. The dayFields object contains three placeholders:

  • @day: This contains the name of the day for the current day. It uses the Day.dayNames array to translate the numeric day to a string. Since each item in Day.dayNames is already translated, the day of the week will be appropriately translated.

  • @satCount: This placeholder is going to produce a string indicating the number of days. We use Drupal.formatPlural() here to handle pluralizing. It will print either is 1 day or are @count days.

  • @saturday: This will contain the translated name of Saturday. This uses the Day.dayNames to get the appropriately translated name.

With the placeholders ready, the next thing the code does is run Drupal.t(). This will take the string Today is @day. There @satCount until @saturday. and substitute in the placeholders.

By the time this part of the code is run, the dayText variable should contain something like Today is Wednesday. There are 3 days until Saturday.

The last step in this function is to insert that generated text into the page. Once again, we are going to use jQuery to do this for us:

var dayDiv = '<div id="day_div"></div>';
$('body').append(dayDiv).children('#day_div').css(divProps)
.text(dayText);

In this snippet, we first define a basic<div></div> element, storing it as a string in dayDiv. Then we use jQuery to do the following:

  1. Find the<body></body> element and build a jQuery object that wraps it.

  2. Append the contents of dayDiv (our div element) to the body.

  3. Search the body element for the child with an ID day_div (which is the ID of the div we added). Essentially, what we are doing here is changing the jQuery object to point to the newly added div element instead of to the body element.

  4. The css() function is called on the jQuery object that wraps the div element. This adds all of the items in divProps as CSS properties. In short, we are now styling the div element.

  5. Finally, by using the text() function we are setting the text content of the div element.

When this long jQuery chain is executed, the HTML will contain a new div element that looks like this:

<div id="day_div" style="padding: 4px; position: absolute; top: 5px; right: 25px; background-color: black; color: white;">
Today is Wednesday. There are 3 days until Saturday.
</div>

Here we've used jQuery to programmatically add a fully styled element with the information we have created.

That wraps up the Day.banner() function. Since we want this to show as soon as the page loads, we need to add one more line to our file:

$(document).ready(Day.banner);

We saw this function in the last chapter. The jQuery ready event fires as soon as the HTML is loaded and the DOM is ready for manipulation. In this case, when that event fires, the Day.banner() function is executed and the weekend countdown is displayed.

Note

Remember that we pass the function object (Day.banner), and not the results of the function (Day.banner()), to the ready() function.

Now we've finished the first part of this project. Regardless of what languages you have installed and your language configuration, the Day.banner() function will always return English text. Why? That's because we have not translated our tool, so every Drupal.t() lookup will fail to find translated text.

Let's fix that by creating a translation for our script.

Translating the project's strings

With our application written to take advantage of the translation system, what we want to do now is provide translations for other languages. To do this, we will use the Translation template extractor module discussed earlier in the chapter.

To do our translation, we will have the template extractor analyze the code in our theme and generate a translation template file. From there, we will simply add the translated text, and then add the translation file to the correct location in the file system.

The first step is to generate the translation template. This is done in Administer | Site building | Translate interface. We are interested in the Extract tab.

For our example, we are going to translate the Frobnitz theme into Spanish. This theme contains the day.js file that we have created as part of this project.

Here is what the Extract tab looks like:

In the previous screenshot, I have already configured things for a download. All I need to do to get my translation template is press the Extract button.

Here's how things work.

The first thing to do is select the part of the site to be translated. I only want to work on the Frobnitz theme, so I have expanded the Directory "sites/all/themes" section and checked the Extract from frobnitz in the sites/all/themes/frobnitz directory radio button.

Next, I have selected the Template language. In the screenshot, there are two available options: Language independent template and Template file for Spanish translation.

The first choice provides a basic template that will work for any language. If I were translating to, say, German (which is not a currently-installed language), then I could choose this option.

Fortunately, since we already have the Spanish Drupal translation installed, we can make use of a shortcut. The second link, Template file for Spanish translations, allows us to generate a template that has already been tailored to our target language.

In addition to this, the last checkbox, Include translations, takes us a step further. It will check to see if there are any existing English-to-Spanish translations that match our own calls.

When the Extract button is pressed, the extractor will analyze our Frobnitz theme. It will locate the text that needs translationthe text in the Drupal.t() function call. Since we checked the Include translations box, it will also search existing translations and try to generate some translations for us.

Once all of this is done, the server will deliver a partially completed translation file.

Here's what the file looks like:

# $Id$
#
# LANGUAGE translation of Drupal (general)
# Copyright YEAR NAME <EMAIL@ADDRESS>
# Generated from files:
# page.tpl.php: n/a
# frobnitz.info: n/a
# day.js: n/a
# test.js: n/a
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"POT-Creation-Date: 2008-08-28 02:47-0600\n"
"PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\n"
"Last-Translator: NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <EMAIL@ADDRESS>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
#: page.tpl.php:20;20;21
msgid "Home"
msgstr "Inicio"
#: frobnitz.info:0
msgid "Frobnitz"
msgstr ""
#: frobnitz.info:0
msgid "Table-based multi-column theme with JavaScript enhancements."
msgstr ""
#: day.js:0
msgid "Sunday"
msgstr "Domingo"
#: day.js:0
msgid "Monday"
msgstr "Lunes"
#: day.js:0
msgid "Tuesday"
msgstr "Martes"
#: day.js:0
msgid "Wednesday"
msgstr "Miércoles"
#: day.js:0
msgid "Thursday"
msgstr "Jueves"
#: day.js:0
msgid "Friday"
msgstr "Viernes"
#: day.js:0
msgid "Saturday"
msgstr "Sábado"
#: day.js:0
msgid "Today is @day. There @satCount until @saturday."
msgstr ""
#: day.js:0
msgid "1 day"
msgid_plural "@count days"
msgstr[0] "1 día"
msgstr[1] "@count días"

While this file is long, it is not very complex. Let's start at the top:

# $Id$
#
# LANGUAGE translation of Drupal (general)
# Copyright YEAR NAME <EMAIL@ADDRESS>
# Generated from files:
# page.tpl.php: n/a
# frobnitz.info: n/a
# day.js: n/a
# test.js: n/a
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"POT-Creation-Date: 2008-08-28 02:47-0600\n"
"PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\n"
"Last-Translator: NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <EMAIL@ADDRESS>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"

Lines that begin with # are comments. The comments are automatically generated and provide some useful information. But there are also a few placeholders that you might want to replace with useful information.

For example, you might want to change Copyright YEAR NAME <EMAIL@ADDRESS> to something like Copyright 2008 Barbara Jenson <bjenson@example.com>.

Next is a block of automatically generated text that provides some metadata about the translation, such as when the translation file was created. Again, you might want to change the default generated value of items, such as Language-Team, to something more accurate.

You will definitely need to set the Plural-Forms directive. This helps Drupal.formatPlural() to correctly set the plural form. The directive takes two parts. First, nplurals indicates how many plural forms the given language has. English and Spanish both have two, Slovenian has four. The second part gives the formula for selecting which of the forms to use. The singular version in our code is the first (0). Plural is in the second place (1). So we can write a simple formula for plural that looks like this: n != 1;. This tells the translator that if n!= 1, the first item (singular) should be used; otherwise, plural should be used.

So the entire Plural-Forms directive should look like this:

"Plural-Forms: nplurals=2; plural=(n != 1);\n"

Note

For the plural formulas of other languages, see http://drupal.org/node/17564.

With the headers out of the way, let's look at the first translated item in the file:

#: page.tpl.php:20;20;21
msgid "Home"
msgstr "Inicio"

These three lines handle the translation of the term"Home".

The first line is an informational comment that tells us where the string appears. It can be found in page.tpl.php on lines 20 and 21.

Here's the actual code from that file (formatted for easier reading):

<?php if ($logo) {
?><a href="<?php print $front_page ?>"
title="<?php print t('Home') ?>"><img src="<?php
print $logo ?>" alt="<?php print t('Home') ?>" /></a>

<?php
} ?>

Notice that both of the highlighted lines call the Drupal PHP function t(), which performs the same task for PHP as Drupal.t() does for JavaScript.

There are a few important things to note about this example:

  • First, this one translation file is handling translations for both the PHP and the JavaScript translations. There is no need to generate different language files for the different technologies.

  • Second, in this case the t() function was used twice with the same string both times. But we only have to translate the string once. This minimizes redundant work.

Looking back at that first entry in the translation file, there are two lines after the comment:

msgid "Home"
msgstr "Inicio"

A translation of any given message is broken into two parts: the original message and the translation. In the previous case, each part takes only one line (longer strings may take multiple lines).

The first line indicates the original English-language string that was passed into the t() function. This is called the message ID.

The second line is the translation into the file's target languageSpanish.

In this case, the translated text was automatically generated. Apparently, the template extractor found another Home message ID and that message was translated to Inicio. So here, that same translation is suggested.

In fact, as we look through the file, we will see that many of the messages we chose have already been translated. All of the day names are done for us.

In fact, the first one that needs translating is this:

#: day.js:0
msgid "Today is @day. There @satCount until @saturday."
msgstr "Hoy es @day. Quedan @satCount til @saturday."

All we need to do to complete this example is translate the string, putting the placeholders in the appropriate places:

Hoy es Jueves. Quedan 2 dias til Sábado.

That's all there is to translating a string.

If, for some reason, we needed to do a multi-line string, and the syntax is something like this:

msgid ""
"Original string"
"More text..."
msgstr ""
"New string"
"More text..."

We should note that the first string is always empty ("") on a multi-line translation.

Now let's take a look at a configuration for a string translated by Drupal.formatPlural():

#: day.js:0
msgid "is 1 day"
msgid_plural " @count days"
msgstr[0] "1 día"
msgstr[1] "@count días"

Again, the extractor found existing values for us, and the translation is already complete. But the format is a little different than other entries.

The msgid line always points to the first string in the Drupal.formatPlural() call. The code that generated the previous configuration was Drupal.formatPlural(i, "1 day", "@count days"). That first string, 1 day, became the message ID.

Beneath that is the original plural form:

msgid_plural " @count days"

Note that in this case, we use msgid_plural instead of just msgid.

The last two lines are the Spanish translations of these two strings:

msgstr[0] "1 día"
msgstr[1] "@count días"

Just as with .info files, an array-like syntax is used here. The first item, msgstr[0], is the singular form. The second item is the plural form.

And that's all there is to handling the plural format translations.

Once we've translated all the strings, we are done with the translation file. Next, we just need to put it in the right place.

Note

What if a term should remain untranslated?

Some of the terms that the extractor finds may be terms you don't want to translate. To leave these terms untranslated, just set the msgstr to be an empty string: msgstr "".

To make the translation file available to Drupal's translation system, you should simply put the translation file in the translations/ directory of your theme (or module). For us, the file is placed in sites/all/themes/frobnitz/translations/. From there, Drupal takes over and we're done.

Note

While developing a translation, you may have to manually reload your translation in order to coerce Drupal into re-parsing the translation files. See the next section for details.

Running the code with the Spanish translation will show text that looks like this:

Hoy es Jueves. Quedan 2 dias til Sábado.

How is this working? Drupal has taken our translation file and built a new JavaScript file (located in sites/default/files/languages/). When the page loads and Spanish is the selected language, Drupal adds a link to that extra JavaScript file:

<script src="/drupal/misc/jquery.js?G" type="text/javascript">
</script>
<script src="/drupal/misc/drupal.js?G" type="text/javascript">
</script>
<script src="/drupal/sites/default/files/languages/es_fc9ac0f50be05d64 034e46fc4de9f518.js?G" type="text/javascript">
</script>

<script src="/drupal/sites/all/themes/frobnitz/printer_tool.js?G" type="text/javascript">
</script>
<script src="/drupal/sites/all/themes/frobnitz/sticky_rotate.js?G" type="text/javascript">
</script>
<script src="/drupal/sites/all/themes/frobnitz/day.js?G" type="text/javascript">

The highlighted section shows the inclusion of the translation JavaScript that Drupal created for us. The contents of that file look like this:

Drupal.locale = {
'pluralFormula': function($n) { return Number(($n!=1)); },
'strings': {
"Thursday": "Jueves",
"Friday": "Viernes",
"Saturday": "Sábado",
"Sunday": "Domingo",
"Monday": "Lunes",
"Tuesday": "Martes",
"Wednesday": "Miércoles",
"1 day": [ "1 día", "@count días" ],
"Test": "carnet",
"Today is @day. There @satCount until @saturday.":"Hoy es @day. Quedan @satCount til @saturday."
}
};

There's no need to take a detailed look at this script. The important thing to note is that it defines an object named Drupal.locale.strings that contains the translation pairs that our script needs.

As Drupal.t() and Drupal.formatPlural() are called, they will check the Drupal.locale.strings object to see if the given string or strings need translation. If they do, then the translation is performed.

Changing a translation file

There is one thing you should be aware of when developing translations for your themes and modules. Once Drupal has scanned your translation PO file once, it will not automatically scan it again. The translation database never gets automatically updated.

Practically speaking, this means that changing the translation file requires an extra step before your changes show up. You will have to manually import the modified translation file.

Note

If you add the new Drupal.t() call to you script, you can walk through the same exporting process we just did in order to create a fresh translation template at Administer | Site building | Translation interface. If you check the Include translation text box, then the translations you already created will be placed in the translation template, and you will not have to recreate any previous work.

Once you have made modifications to your translation file, you can re-import it by going to Administer | Site building | Translation interface and clicking on the Import tab, which will show the following screen:

In the previous screenshot, we are re-uploading the frobnitz.es.po.txt file that we created before. Note that under the Mode section, I have selected the first option (Strings in the uploaded file replace existing ones, new ones are added) so that any changes I've made are given priority.

Just remember to save the updated file in your theme so that when you install the theme elsewhere, the correct version will be imported.

Summary

In this chapter, we focused on the translation system in Drupal, and the JavaScript tools that are used in conjunction with that system. We looked at installing and configuring multiple languages using the JavaScript Drupal.t() and Drupal.formatPlural() functions, and then extracting and translating the strings. In this chapter's project, we focused on using these translation functions, and then created a new translation to use for testing.

Now, we've seen an important and powerful aspect of Drupalone that has been made accessible to JavaScript just recently (with Drupal 6). Next, we'll turn to another set of tools in the drupal.js file.