Skip to content

Making Plugins and Themes Translation-Ready

Internationalization makes WordPress accessible in other languages, and it’s a must-have for work intended for wide distribution.

As WordPress continues to grow, not just in English-speaking countries but around the world, a tension grows: most programming is done in English, and most WordPress add-ons (either plugins or themes) are written with English strings. However, many WordPress users—now significantly more than 50%—are not first-language English speakers.

This is why, over the last decade, we’ve seen more and more discussion of and support for internationalization. Internationalization makes WordPress accessible in other languages, and it’s a must-have if you’re working on themes or plugins for wide distribution.

Term Note: “i18n” and “l10n”

Because this took me a while to understand: You’ll frequently see “internationalization” abbreviated as “i18n.” This is because, between the opening “i” and final “n” of the word internationalization, eighteen letters exist. So you’re skipping 18 of the 20 letters in the word, and replacing them with the numeral of the count.

Localization, the word for taking advantage of internationalization, is often abbreviated as l10n, with the same logic. Again, “l-ten-n” doesn’t itself mean anything special; spelling it that way is just a clever numeral thing.

The Steps to Internationalization for WordPress Code

Making it easy to translate a plugin or theme, with its native English strings for interface elements, requires three steps:

  1. Decide on, declare, and use a “text domain.”
  2. Use the relevant functions with your strings and that text domain.
  3. That allows you to generate translation files.

Armed with those translation files, people can translate your plugin or theme.

What Is a WordPress “Text Domain”?

A text domain is a way to segregate one set of “strings to translate” from other strings in the broader environment.

A text domain is the “translation namespace” for your plugin or theme. It’s a way for WordPress, your plugin, or your theme to segregate its list of “strings to translate” from other strings that might need to be translated in other parts of the broader environment.

Why is a text domain necessary or useful? Because different strings may mean different things in different plugins or themes. For example, “Let’s go!” could mean “Create a membership account” in one context and “Start the survey” in a different context.

So you declare a text domain, and use it with all your “gettext” or translating functions, which we explain in depth in the next section. That’s not too hard. (The established gettext project underpins the way the whole WordPress translation system works.)

You’ll want to list your text domain in your plugin or theme’s top comment block, located in style.css for themes, or in your plugin’s main PHP file. That looks like this for plugins:

<?php
/*
Plugin Name: Pretend Plugin
[Other comment-block information goes here]
Text Domain: wpshout
*/

Or like this for themes:

/*
Theme Name: Pretend Theme
[Other comment-block information goes here]
Text Domain: wpshout
*/

Gettext Functions: Keeping Your __()s, _e()s, _x()s, and _n()s Straight

So to make your texts translatable, they need to pass though a function that might replace “Let’s go!” with the Spanish, Korean, or Swahili equivalent. And for the sake of convenience, WordPress gives all of the functions that accomplish this goal very short names.

__()

I think of the double underscore function, __() as the master one. It’s got the most confusing name — but its basic goal is to do exactly what I described: to allow all strings to be translated into a language if they should be. You use it by passing it two parameters: your string, and your text domain. So it’ll look like:

echo __( 
    'WPShout is a great WordPress site!',
    'wpshout'
);

Auto-Echoing With _e()

The double-underscore just returns the string; you have to echo it yourself. If that’s annoying you, _e() can help: it’s the equivalent of echo __(). It saves you a few characters, but doesn’t do anything else.

Our example, again:

_e( 
    'WPShout is a great WordPress site!',
    'wpshout'
);

Then, you deal with some of the finer points of translation.

Providing Context With _x()

First, sometimes a word or phrase is pretty ambiguous. Let’s imagine, as a translator, you’re asked to just translate the word “post” into Arabic. Well, was that referring to a noun, like “a WordPress post”? Or maybe it was a noun, but a lamp post? Or possibly you meant the verb “to post,” which is the equivalent of “to publish.”

Unlike __( 'Post', 'wpshout' ) and _e( 'Post', 'wpshout' ), where you’d leave the translator uncertain about the meaning, you can provide a context with the _x() function. It looks like:

_x( 
    'Post', 
    'verb, as in "to publish"',
    'wpshout'
);

The middle parameter string is never presented to the user, but will show to translators to help them understand. (Also worth knowing, an _ex() function exists, that as you might guess is the combinations of the _e and _x functions: it prints the translated, annotated string.)

Pluralization With _n()

Another place that languages get complicated is around pluralization. In English, I have “1 Comment” or “3 Comments”. To account for that, WordPress offers the _n() function, which can process the difference between the two.

Using _n() usually requires that you make another call, to the printf() (or sprintf — the s means it returns a string, while printf basically echos for you) function. These — also strangely named — functions replace a token in a string with a value passed to them. So to use our comments example, it’ll looks something like:

printf( 
    _n( 
        'One comment', 
        '%s comments', 
        $comments, 
        'wpshout'
    ),
    $comments
);

You’ll note we’re using a numerical value, assigned to the variable $comments twice. Let’s say $comments has a value of 3. The first use of $comments is for _n to determine which pluralization to use: since 3 is more than 1, it’ll pick %s comments rather than 'One comment'.

The second use is for printf to actually put that number into the string, replacing the %s with the 3 that $comments was set to. So we’d print “3 comments” in this case.

Other Gettext Functions

This covers all the essentials of the “gettext” functions, but there are others worth noting:

  • esc_attr__(), which will do the same as __(), but it’ll take care of esc_attr() (escaping HTML attributes) for you for safety’s sake. (Also in existence are esc_attr_e() and esc_attr_x(): those auto-echo and allow for annotations, as you’d expect.)
  • esc_html__(): again, for safety’s sake you can combine your HTML escaping with your translation. (And again, esc_html_e() and esc_html_x() variations exist.)
  • _n_noop() which “registers but doesn’t translate.” In other words, it does the same thing as _n() with “no operation.” For more detail, or just some interesting insight into translation’s finer points, I can point you to this post from Konstantin Kovshenin.

Handing Translations in JavaScript with wp_localize_script()

Because translations in WordPress are handled via PHP, you need to pass your translated strings to your JavaScript usages. This is done through the wp_localize_script() function. The function is extremely useful and much-used outside of translation, because it lets you pass any PHP value or variable to JavaScript. But translation is the use for which it was named.

In all cases, you use wp_localize_script like so:

wp_localize_script( 
    'wpshout-js', 
    'strings', 
    array(
        'hello' => __( 'Hello!', 'wpshout' );
    )
);

Then to access the values in your JavaScript, you’ll do something like:

alert( strings.hello );

wp_localize_script() has three parameters:

  1. The handle we used in wp_enqueue_script() (or wp_register_script()) for the JS we’re localizing
  2. The name of JavaScript object we want our translations bound to
  3. An associative array of the translations, with “index” or property name as the key, and the value as the actual string to be used

The translation itself is handled by the same PHP functions we covered in the last section.

Technical Details: what are POT, PO, and MO files?

So far we’ve mainly been talking about programming PHP. But there’s a secondary part of the whole gamut that doesn’t really involve authoring PHP at all, and that’s generating translation files. Nowadays, WordPress.org has streamlined this process for plugins released there. But what’s going on under the hood is a little confusing.

All WordPress translations are actually files under the hood. Whatever form you used to create, edit, and read these translation files, there are three core types:

  • POT (.pot) files: master files for a plugin or theme, which have gathered up all the strings from your __()s and _ex()s, etc. These are then scooped up in the generation of…
  • PO (.po) files, which are the plaintext translations of your English strings into the target language. You’ll rarely edit one directly — the format has a lot of noisy and intimidating parts — but you can open them in a text editor to make quick tweaks to the translation.
  • MO (.mo) files: the final translations from English into the target language which are actually used and read by WordPress. These are binary files, so while you open can one in a text editor it won’t look like anything you’ll be editing… You actually you have to instead go back to the PO file and regenerate the MO file from there.

As I said, the generation and use of these files is one of the harder parts of i18n for me. But if your theme or plugin’s in the WordPress.org repository, they’ll take care of the .pot file for you, and then your translators will likely know how to manage the rest. If you need more details or want other tools, check out the “Translating WordPress” page.

What We’ve Learned About Internationalizing WordPress Code

We’ve covered the three core parts of making your WordPress code translation-ready: decide a text-domain, use it in your “gettext” functions, and then get a hold of the POT file to make your PO and MO files. The steps are each small and doable, and they make a big impact for non-English speakers, or those who are simply more comfortable in a different language they learned earlier. Happy hacking!

Yay! 🎉 You made it to the end of the article!
David Hayes
Share:

9 Comments
Most Voted
Newest Oldest
Inline Feedbacks
View all comments
Reinhard
November 8, 2020 10:16 am

Great Article – Thanx!

Building a Magical Golden Bridge from PHP to JavaScript with wp_localize_script() | WPShout
September 1, 2015 3:08 pm

[…] other words, wp_localize_script()started out as an i18n thing, and turned out to be massively useful for a lot of other […]

This Week in WordPress: LoopConf a Success and Angels Swoop on Receiptful - Daniele Milana
May 15, 2015 6:24 pm

[…] Making Plugins and Themes Translation-Ready (WP Shout). […]

This Week in WordPress: LoopConf a Success and Angels Swoop on Receiptful - WPMU DEV
May 14, 2015 6:22 pm

[…] Making Plugins and Themes Translation-Ready (WP Shout). […]

This Week in WordPress: LoopConf a Success and Angels Swoop on Receiptful - WordPress Community | powered by Mpress Studios
May 14, 2015 5:15 pm

[…] Making Plugins and Themes Translation-Ready (WP Shout). […]

This Week in WordPress: LoopConf a Success and Angels Swoop on Receiptful | Blue Digital Web Services
May 14, 2015 12:52 pm

[…] Making Plugins and Themes Translation-Ready (WP Shout). […]

nomad411
May 12, 2015 5:58 pm

Excellent article. I,ve already started sharing it where I can, in developer groups, on IRC…

Rachel R. Vasquez
May 12, 2015 4:36 pm

FINALLY someone’s explained all this for me! Thanks! I’ve been using the _e( ) function as I’ve seen it in other standard themes but wasn’t aware of the other options. Also didn’t know how to go as far as the JS. Thanks for this. 🙂

fredclaymeyer
May 13, 2015 4:21 am

I thought David did a great job here, too. Glad you guys liked it. 🙂

Or start the conversation in our Facebook group for WordPress professionals. Find answers, share tips, and get help from other WordPress experts. Join now (it’s free)!