I18n for WordPress Developers

跳转至: 导航搜索

What is I18n?

I18n is a abbreviation for internationalization, or the process of making an application ready for translation. It is called "i18n" because there are 18 letters between the i and the n. In the WordPress case, it means marking strings which should be translated in a special way.

Introduction to Gettext

WordPress uses the gettext libraries and tools for i18n. Note that if you look online, you'll see the _() function which refers to the native PHP gettext-compliant translation function, but instead with WordPress you should use the __() wordpress-defined PHP function.

Translatable strings

In order to make a string translatable in your application you have to just wrap the original string in a __() function call:

$hello =  __( 'Hello, dear user!', 'my-text-domain' );

If your code should echo the string to the browser, use the _e function instead:

_e( 'Your Ad here', 'my-text-domain' )

POT files

After the strings are marked in the source files, a gettext utility called xgettext is used to extract the original strings and to build a template translation POT file. Here is an example POT file entry:

#: wp-admin/admin-header.php:49
msgid "Sign Out"
msgstr ""

PO files

Every translator takes the WordPress POT file and translates the msgstr sections in their own language. The result is a PO file with the same format as a POT, but with translations and some specific headers.

MO files

From a translated PO file a MO file is built. This is a binary file which contains all the original strings and their translations in a format suitable for fast translation extraction. The conversion is done using the msgfmt tool.

A typical msgfmt command looks like this: msgfmt -o filename.mo filename.po

If you have a lot of PO files to convert at once, you can run it as a batch. For example, using a bash command:

# Find PO files, process each with msgfmt and rename the result to MO
for file in `find . -name "*.po"` ; do msgfmt -o `echo $file | sed s/\.po/\.mo/` $file ; done

Text Domains

If you're translating a plugin or a theme, you'll need to use a text domain to denote all text belonging to that plugin/theme. This increases portability and plays better with already-existing WordPress tools. (See the section below on Plugins.)

In general, an application may use more than one large logical translatable module and a different MO file accordingly. A domain is a handle of each module, which has a different MO file.

If you want to get a broader and deeper view of gettext, we recommend you read the gettext online manual.

Marking Strings for Translation

The strings for translation are wrapped in a call to one of a set of special functions. The most commonly used one is __(). It just returns the translation of its argument:

echo '<h2>' . __( 'Blog Options', 'my-text-domain' ) . '</h2>';

Another simple one is _e(), which outputs the translation of its argument. Instead of writing:

echo __( 'Using this option you will make a fortune!', 'my-text-domain' );

you can use the shorter:

_e( 'Using this option you will make a fortune!', 'my-text-domain' );


echo 'We deleted $count spam messages.'

How would you i18n this line? Let's give it a try together:

_e( 'We deleted $count spam messages.', 'my-text-domain' );

It won't work! Remember, the strings for translation are extracted from the sources, so the translators will see work on the phrase: "We deleted $count spam messages.". However in the application _e will be called with an argument like "We deleted 49494 spam messages." and gettext won't find a suitable translation of this one and will return its argument: "We deleted 49494 spam messages.". Unfortunately, it isn't translated correctly.

The solution is to use the printf family of functions. Especially helpful are printf and sprintf. Here is what the right solution of the spams count problem will look like:

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

Notice that here the string for translation is just the template "We deleted %d spam messages.", which is the same both in the source and at run-time.

If you have more than one placeholder in a string, it is recommended that you use argument swapping. In this case, single quotes (') are mandatory : double quotes (") will tell php to interpret the $s as the s variable, which is not what we want.

printf( __( 'Your city is %1$s, and your zip code is %2$s.', 'my-text-domain' ), $city, $zipcode );

Here the zip code is being displayed after the city name. In some languages displaying the zip code and city in opposite order would be more appropriate. Using %s prefix in the above example, allows for such a case. A translation can thereby be written:

printf( __( 'Your zip code is %2$s, and your city is %1$s.', 'my-text-domain' ), $city, $zipcode );


Let's get back to the spams example. What if we delete only one spam? The output will be: We deleted 1 spam messages., which is definitely not correct English, and would certainly be incorrect for many other languages as well.

In WordPress you can use the _n() function.

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

_n() accepts 4 arguments:

  • 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 (there are languages, which have far more than 2 forms)

The text domain is the fourth optional parameter. The return value of the functions is the correct translated form, corresponding to the given count.

Disambiguation by context

Sometimes one term is used in several contexts and 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 (Click here to post your comment) and as a noun (Edit this post). In such cases the _x() function should be used. It is similar to __(), but it has an additional second argument -- the context:

if ( false === $commenttxt ) $commenttxt = _x( 'Comment', 'noun' );
if ( false === $trackbacktxt ) $trackbacktxt = __( 'Trackback' );
if ( false === $pingbacktxt ) $pingbacktxt = __( 'Pingback' );
// some other place in the code
echo _x( 'Comment', 'column name' );

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 contexts.

The text domain is the third optional parameter:

echo _x( 'Comment', 'column name', 'my-text-domain' );

Note that similarly to __(), _x() has an 'echo' version: _ex(). The previous example could be written as:

_ex( 'Comment', 'column name', 'my-text-domain' );

Use whichever you feel enhances legibility and ease-of-coding.


Do you think translators will know how to translate a string like:

__( 'g:i:s a' )

? In this case you can add a clarifying comment in the source code. It has to start with the words translators: and to be the last PHP comment before the gettext call. Here is an example:

/* translators: draft saved date format, see http://php.net/date */
$draft_saved_date_format = __( 'g:i:s a' );

By adding a translators: comment you can write a "personal" message to the translators, so that they know how to deal with the string.

Newline characters

Gettext doesn't like \r (ASCII code: 13) in translatable strings, so please avoid it and use \n instead.

Empty strings

The empty string is reserved for internal Gettext usage and you must not try to internationalize the empty string. It also doesn't make any sense, because the translators won't see any context.

If you have a valid use-case to internationalize an empty string, add context to both help translators and be in peace with the Gettext system.

Handling JavaScript files

Use wp_localize_script() to add translated strings or other server-side data to a previously enqueued script.

wp_enqueue_script( 'script-handle', … );
wp_localize_script( 'script-handle', 'objectL10n', array(
	'speed'  => $distance / $time,
	'submit' => __( 'Submit', 'my-text-domain' ),
) );

Then in the JavaScript file, corresponding to script-handle you can use objectL10n.variable:

$('#speed').val('{speed} km/h'.replace('{speed}', objectL10n.speed));

Best Practices

Until we gather some WordPress-specific examples, use your time to read the short, but excellent article in the gettext manual. Summarized, it looks like this:

  • Decent English style—minimize slang and abbreviations.
  • Entire sentences—in most languages word order is different than that in English.
  • Split at paragraphs—merge related sentences, but do not include a whole page of text in one string.
  • Use format strings instead of string concatenation—sprintf(__('Replace %1$s with %2$s'), $a, $b); is always better than __('Replace ').$a.__(' with ').$b; .
  • Avoid unusual markup and unusual control characters—do not include tags that surround your text and do not leave URLs for translation, unless they could have a version in another language.
  • Do not leave leading or trailing whitespace in a translatable phrase.

Translating Plugins

If you're trying to translate a plugin, the same advice as above applies, except that

  • you must use a domain, which is loaded in a hook of your plugin
  • every translation call must become __('Text', 'my-text-domain')
  • your domain name, which probably will resemble or be the same as your plugin name, must include no underscores.

Choosing and loading a domain

The text domain is a unique identifier, which makes sure WordPress can distinguish between all loaded translations. Using the basename of your plugin is always a good choice.

Example: if your plugin is a single file called shareadraft.php or it is contained in a folder called shareadraft the best domain name you can choose is shareadraft. In case of a theme — choose the directory name.

The text domain name is also used to form the name of the MO file with your plugins' translations. You can load them by calling the function load_plugin_textdomain as early as the plugins_loaded action.

load_plugin_textdomain( $domain, $path_from_abspath, $path_from_plugins_folder )


function myplugin_init() {
 $plugin_dir = basename(dirname(__FILE__));
 load_plugin_textdomain( 'my-plugin', false, $plugin_dir );
add_action('plugins_loaded', 'myplugin_init');

This call tries to load my-plugin-{locale}.mo from your plugin's base directory. The locale is the language code and/or country code you defined in the constant WPLANG in the file wp-config.php. For example, the locale for German is 'de', and the locale for Danish is 'da_DK'. From the code example above the text domain is 'my-plugin' therefore the Danish MO and PO files should be named my-plugin-da_DK.mo and my-plugin-da_DK.po. For more information about language and country codes, see Installing WordPress in Your Language.

  • For WordPress 2.6 and up, the third parameter is the directory containing the .mo file, relative to the plugins directory. It must end with a trailing slash. If your plugin doesn't need compatibility with older versions of WordPress, you can leave the second parameter blank.
  • For versions lower than 2.6, the second parameter should be the directory containing the .mo file, relative to ABSPATH. The third parameter should be blank.

For themes the process is surprisingly similar:


Put this call in your functions.php and it will search your theme directory for locale.mo and load it (where locale is the current language, i.e. pt_BR.mo).

Watch Out

  • DO name your MO file as locale.mo (e.g., da_DK.mo)
  • DO NOT name your MO file as my_theme-da_DK.mo

Translate meta data

If you add a line like this to your plugin or theme header where the theme or plugin name is, WordPress should internationalize your plugin or theme meta-data when it displays your plugin in the admin screens:

Text Domain: my-text-domain

I18n for widgets developed on 2.8+

WordPress 2.8+ uses a new Widget API, that only requires the widget developer to extend the standard widget class and some of its functions. With this API there is no init function. After the widget is coded using the widget(), form(), and update() methods, the widget must be registered. The textdomain is then loaded after the widget is registered.


// register Foo_Widget widget
function Foo_Widget_init() {
    return register_widget( 'Foo_Widget' );
add_action( 'widgets_init', 'Foo_Widget_init' );

$plugin_dir = basename( dirname( __FILE__ ) );
load_plugin_textdomain( 'foo_widget', null, $plugin_dir );

This example registers a widget named Foo_Widget, then sets the plugin directory variable and attempts to load the foo_widget-locale.po file.

Marking strings in themes and plugins

All the rules from above apply here, but there is one more. The additional rule states that you must add your domain as an argument to every __, _e and __ngettext call, otherwise your translations won't work.


  • __('String') should become __('String', 'text_domain')
  • _e('String') should become _e('String', 'text_domain')
  • _n('String', 'Strings', $count) should become _n('String', 'Strings', $count, 'text_domain')

Adding the domain by hand is a burden and that's why you can do it automatically:

  • If your plugin is registered in the official repository, go to your Admin page there and scroll to Add Domain to Gettext Calls.


php add-textdomain.php -i domain phpfile phpfile ...

After it's done, the domain will be added to all gettext calls in the files.

Generating a POT file

You remember the POT file is the one you need to hand to translators, so that they can do their work, don't you?

There are a couple of ways to generate a POT file for your plugin: 1 If your plugin is registered in the official repository, go to your Admin page there and then click on Continue by the "Generate POT file" section. 文件:Plugin Admin Area.png

Then simply click on Get POT to download the POT file. 文件:Plugin Generate POT Area.png

2 If your plugin is not in the repository, you can checkout the wordpress-i18n tools directory from SVN (see Using Subversion to learn about SVN) and then run the makepot.php script like this:

php makepot.php wp-plugin your-plugin-directory

You need the gettext (GNU Internationalization utilities) package to be installed in your server before you can run the above command (and you must use svn to check out the wordpress-i18n tools, rather than downloading them from the URL above). After it's finished you should see the POT file in the current directory.

To generate the POT file for a theme, use:

php makepot.php wp-theme your-theme-directory

It is a good idea to offer the POT file along with your plugin or theme, so that translators won't have to ask you specifically about it.