WordPress i18n: Make Your Plugin Translation Ready

In a previous post, I covered the fundamentals of WordPress internationalization (abbreviated as i18n); how to install a localized version of WordPress and how an existing WordPress site can be easily converted to a localized version.

In this article, I’m going to walk you through the process of Internationalizing WordPress plugins. The process isn’t difficult and once the knowledge is acquired, you can have your WordPress plugins easily translated to other languages.

Difference Between Internationalization and Localization

Over the years, developers tend to misconstrue the meaning of these terms – Internationalization and Localization.

  • Internationalization is the process of developing your plugin so it can easily be translated into other languages.
  • Localization describes the subsequent process of translating an internationalized plugin to a new language.

It is worthy of note that Internationalization is often abbreviated as i18n (because there are 18 letters between the ‘i’ and the ‘n’) and Localization is abbreviated as l10n (because there are 10 letters between the ‘l’ and the ‘n’.)

Why Internationalize?

The answer is simple; WordPress is used all over the world in many different languages. When plugins are internationalized, they attract a larger audience from other parts of the world who would obviously benefit by using the plugin in their own language.

As a developer, you may not have time to provide localized versions of your plugin because you do not speak other languages. However, when you internationalize your plugin, you leave the door open for others to create localization without necessarily modifying the source code.

Plugin Internationalization

Now that we are familiar with the concept of plugin internationalization and localization, let’s dive into the process of making plugins ready for translation.

Set Translation Headers

The first step to take in making a plugin translatable is to include the translation headers among the plugin headers.

The translation headers are the Text Domain and the Domain Path.

The Text Domain is used to denote all text belonging to a plugin.
It is a unique identifier which ensures WordPress can distinguish between all loaded translations. This increases portability and plays better with already existing WordPress tools.

The text domain must match the slug of the plugin. For example, if your plugin is a single file called sample-plugin.php or it is contained in a folder called sample-plugin the text domain should be sample-plugin.

A Note on Text Domain

The text domain name must use dashes and not underscores.

Remember I said the text domain must match with the plugin slug? That may not be true after all. I carried out a quick experiment with one of my plugins, instead of the plugin slug, I used a unique text and it worked without any problems.

Moral: ensure the Text Domain is unique so it doesn’t clash with that of other plugins.

The Domain Path is the folder WordPress will search for the .mo translation files.

By default, WordPress searches the plugin directory for the translation files to use. Having the translation file at the root folder of your plugin could disorganize your plugin structure.

If you wish to keep the translation files in a folder, for example; /languages, you need to inform WordPress about it using the Domain Path header.

Below is a typical header of a WordPress plugin including that of the translation.

<?php
/*
 Plugin Name: Enable Shortcode and PHP in Text widget
 Plugin URI: http://w3guy.com/shortcode-php-support-wordpress-text-widget/
 Description: Enable shortcode support and execute PHP in WordPress's Text Widget
 Author: Agbonghama Collins
 Version: 1.2
 Author URI: http://w3guy.com
 Text Domain: espw-plugin
 Domain Path: /languages/
 */

Load Text Domain

Now, we would use the load_plugin_textdomain() function to tell WordPress to load a translation file if it exists for the user’s language.

Below is the function synopsis.

<?php load_plugin_textdomain( $domain, $abs_rel_path, $plugin_rel_path ) ?> 

The first parameter $domain should be the text domain; the $abs_rel_path has been deprecated and should be set to false; finally, $plugin_rel_path is the relative path to translation files.

If translation MO files is in the plugin’s own directory, use as follows:

load_plugin_textdomain( 'espw-plugin', false, dirname( plugin_basename( __FILE__ ) ) );

If translation MO files is in the plugin’s languages subdirectory. Use as follows:

load_plugin_textdomain( 'espw-plugin', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );

You don’t just call the load_plugin_textdomain function, it should be called in your plugin as early as the plugins_loaded action like this:

function load_plugin_textdomain() {
  load_plugin_textdomain( 'espw-plugin', FALSE, basename( dirname( __FILE__ ) ) . '/languages/' );
}

add_action( 'plugins_loaded', 'load_plugin_textdomain' );

Deep Dive into Plugin I18n

Now that the Text Domain and the Domain Path header is set, it’s time to learn how to internationalize a plugin.

This segment of the tutorial will be divided into the following:

  1. Strings translation
  2. Using Placeholders
  3. HTML translation
  4. Dealing with Plurals
  5. Disambiguation by context
  6. Escaping translation strings

Please note: The string espw-plugin will be use as text domain in this tutorial.

1. Strings Translation

To make a string translatable in your plugin, wrap the original string in a __() function call as follows:

$text =  __( 'Hello, SitePoint Readers!', 'espw-plugin' );

If you want to echo the string to the browser, instead of the echo language construct, use the _e function:

_e( 'Hello, SitePoint Readers!', 'espw-plugin' );

2. Using Placeholders

As PHP and WordPress developers, I assume you know what placeholders are. You can quickly skim through the sprintf and printf() PHP documentation for further information.

If you are using variables in strings like the example below, you should use placeholders.

echo 'Your city is $city.'

The right way is to use the printf()function as follows:

printf(
    __( 'Your city is %s.', 'espw-plugin' ),
    $city
);

Going through the code of some plugins hosted in the WordPress plugin repository, I do see things like this:

echo __('Your city is $city', 'espw-plugin');
_e('Your city is $city', 'espw-plugin');

Although the strings are now translatable, the PHP variable $city also becomes translatable.

This is a bad practice because a translator could mistakenly alter the variable or inject malicious code into the plugin code base, and this will ultimately make the plugin malfunction.

The sprintf function is similar to the printf in that they format string using placeholders while printf outputs a formatted string, the sprintf return the string.

Example: the following code assign a formatted string to the variable $text.

$text = sprintf( __('Your city is %s.', 'espw-plugin'), $city );

3. HTML Translation

Including HTML in translatable strings depends on the context.

An example is a link (separated from text surrounding it):

<div class="site-info">
  <a href="http://wordpress.org/" >< ?php _e( 'Proudly powered by WordPress.', 'espw-plugin' ); ?></a>
</div>

Another example is a link in a paragraph (not separated from text surrounding it):

<?php
$url = 'http://example.com';
$link = sprintf( __( 'Check out this link to my <a href="%s">website</a> made with WordPress.', 'espw-plugin' ), $url );
echo $link;
?>

4. Dealing with Plurals

A string that changes when the number of items changes can be internationalized using the _n() function.

This function accepts 4 arguments, namely:

  • Singular – the singular form of the string
  • Plural – the plural form of the string
  • Count – the number of objects, which will determine whether the singular or the plural form should be returned
  • Text Domain – the plugins text domain

Let’s see some examples to comprehend how the _n() function works.

In English you have "One comment" and "Two comments". In other languages you can have multiple plural forms.

The code below demonstrates how to handle such scenario using the _n() function.

printf(
    _n(
        'One comment',
        '%s comments',
        get_comments_number(),
        'espw-plugin'
    ),
    number_format_i18n( get_comments_number() )
);

Code Explanation the code above consist of these three functions – printf, _n and number_format_i18n.

For easy assimilation, the function code will be dissected with each function component explained.

 _n(
        'One comment',
        '%s comments',
        get_comments_number(),
        'espw-plugin'
    )

The first argument passed to the _n function is the text to be displayed when the number of comments is singular.

The second is the text displayed when the number of comments is greater than one.

The placeholder %s will contain the value of number_format_i18n( get_comments_number() ) which will be explained shortly.

The third argument get_comments_number() is assumed to be a function that returns the comment count.

If it returns 1, the first argument One comment get outputted by printf otherwise the second argument %s comments is returned if it is greater than 1.

Please note: The placeholder %s gets replaced by the integer returned by number_format_i18n( get_comments_number() ) which is the second argument passed to the printf function.

Finally, the fourth argument is the translation text domain.

The function number_format_i18n() converts the comment count to format based on the locale. See the documentation for more information.

Similar to the number_format_i18n() is the date_i18n that retrieves the date in localized format, based on timestamp.

Still on the _n() function, below is another demonstration of how the function works.

printf( _n( 'We deleted one spam message.', 'We deleted %d spam messages.', $count, 'my-text-domain' ), $count );

If the variable $count returns 1, the text We deleted one spam message will be displayed; but if it is greater than 1, We deleted %d spam messages will be displayed with the placeholder %d replaced by the integer value of $count.

5. Disambiguation by Context

Sometimes one term is used in several contexts, although it is one and the same word in English it has to be translated differently in other languages.

For example the word Post can be used both as a verb as in "Click here to post your comment" and as a noun "Edit this post".

In such cases the _x or _ex function should be used.

It is similar to __() and _e(), but it has an extra argument — $context.

_x( 'Post', 'noun', 'espw-plugin' );
_x( 'Post', 'verb', 'espw-plugin' );

Using this method in both cases we will get the string Comment for the original version, but the translators will see two Comment strings for translation, each in the different context.

When the strings made translatable by the function _x() get parsed by a translation tool such as Poedit, the context argument provides a hint to the translator on the context the string/text was used.

In German, the Post as a noun is Beitrag while as a verb is verbuchen.

Below is a screenshot of Poedit translating the string Post to German with the context squared-bracketed.

Poedit Translating to German

While _x() retrieve translated string, _ex() displays it.

6. Escaping Translation Strings

WordPress has a number of functions for validating and sanitizing data.

Among the list are functions for escaping translation texts – esc_html(), esc_html_e(), esc_html_x(), esc_attr(), esc_attr_e() and esc_attr_x(). You can get more information on each of these functions at WordPress Codex.

I don’t need to explain every one of these, but what they do is basically escape translatable texts.

Wrap Up

One of the goals of WordPress is to make it easy for users across the world to publish content. As a plugin developer, you can help to further ease the publishing process for users when you internationalize your plugins.

The first part of this tutorial was essentially about everything you need to know about plugin i18n.

The concluding part will be a walk-through on how to make a plugin translation ready as well as learning how to localize a plugin to a new language.

I do hope you have learned something new from this tutorial.

Happy coding!

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

No Reader comments

Comments on this post are closed.