Rolling Up Your Sleeves and Getting into the Nitty Gritty of I18n in WordPress

Tweet

Last week, I covered the basic components of i18n in WordPress and how each of those pieces fit together.

Now let’s dig a little bit deeper and take a look at some real code, shall we?

Localizing a Theme in WordPress

When you are localizing a theme, you’ll usually only be addressing text strings that appear in different places of your theme.

Therefore, let’s have a look at some basic HTML code that has a few strings in it:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Localization Sample</title>
</head>

<body>
<p>My name is Mick.</p>
<p>I have a dog named Lacie.</p>
<p>My dog's name is Lacie, but we call her Bug.</p>
<p>Sometimes, we call her Buggers.</p>
<p>Lacie has a black coat.</p>

</body>
</html>

Not much to see here, really. We just have five different strings of text entered into an HTML document in a pretty generic way. However, we can localize this page by simply wrapping each of the strings with the _e() function.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Localization Sample</title>
</head>

<body>
<p><?php _e( 'My name is Mick.', 'our-very-unique-domain' ); ?></p>
<p><?php _e( 'I have a dog named Lacie.', 'our-very-unique-domain' ); ?></p>
<p><?php _e( 'My dog's name is Lacie, but we call her Bug.', 'our-very-unique-domain' ); ?></p>
<p><?php _e( 'Sometimes, we call her Buggers.', 'our-very-unique-domain' ); ?></p>
<p><?php _e( 'Lacie has a black coat.', 'our-very-unique-domain' ); ?></p>

</body>
</html>

This is a bit more interesting now. We’ve wrapped each text string with _e() and set our textdomain constant for the localization. Note that I have used a constant called our-very-unique-domain. As you begin to localize your own themes and plugins, just make sure you realize that it really doesn’t matter what you call this domain so long as it is unique to you, and you initialize the relationship with the same unique name. How do we initialize the relationship within functions.php? Let’s look at the code:

<?php
load_theme_textdomain( 'our-very-unique-domain', TEMPLATEPATH.'/languages' );

$locale = get_locale();
$locale_file = TEMPLATEPATH."/languages/$locale.php";
if ( is_readable($locale_file) )
	require_once($locale_file);
?>

As you can see, on line 1 we’ve fired up load_theme_textdomain() and specified that our language translation files will live in the /languages folder of our theme. So far, so good, but now we see bunch of stuff that talks about locale. Theme localization depends on WPLANG constant in wp-config.php which defines the locale.

The locale is a combination of both a country and a language code specified by the GNU gettext framework – you can look up country and language abbreviations in the gettext manual.

Open up your wp-config.php file and look to see if you have a custom WordPress locale defined … if you don’t, go ahead and define it now. For example, if you are using German as the main language for your site, the you would see (or manually add) a line in your wp-config.php file like this:

define ( 'WPLANG', 'de_DE');

With the WordPress locale set, (in this case de_DE), our code above will now seek to find a German localization file called de_DE.mo in the /languages directory of our theme. Therefore, the files in our sample theme directory might ultimately have a structure that looks something like this:

sample theme localization file structure

Localizing a Plugin in WordPress

Localizing a plugin is very similar to localizing a theme. Let’s take it from the top by looking at a very simple plugin that has not been localized.

<?php
/*
Plugin Name: Our Sample Plugin
Plugin URI: http://www.sitepoint.com/our-sample-plugin
Description: Sample localization code demonstration
Version: 1
Author: Mick Olinik
Author URI: http://www.sitepoint.com
License: GPL2
*/ 

add_action( 'init', 'olin_osp_init' );
function olin_osp_init() {
	add_action( 'admin_menu', 'olin_osp_menu' );
}

function olin_osp_menu() {
	add_options_page( 'Our Sample Plugin Options', 'Our Sample Plugin', 'manage_options', 'our-sample-plugin', 'olin_osp_settings' );
}

function olin_osp_settings() {
	?>
	<div class='wrapper'>
		<h1>Our Sample Plugin Settings</h1>
		<!-- Imagine that there is some really exciting functionality happening here -->
	</div>
	<?php
}
?>

Again, not much to see here really. We’re simply registering our plugin with WordPress, creating an admin menu for our users, and then adding an admin page to modify the settings of our sample plugin.

However, when we move to localize the plugin, we’ll want to make some key changes. Let’s look at the same plugin with correctly localized code.

<?php
/*
Plugin Name: Our Sample Plugin
Plugin URI: http://www.sitepoint.com/our-sample-plugin
Description: Sample localization code demonstration
Version: 1
Author: Mick Olinik
Author URI: http://www.sitepoint.com
License: GPL2
*/ 

add_action( 'init', 'olin_osp_init' );
function olin_osp_init() {
	add_action( 'admin_menu', 'olin_osp_init' );
	load_plugin_textdomain( 'our-very-unique-domain', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
} 

function olin_osp_menu() {
	add_options_page( sprintf( __( '%s Options', 'our-very-unique-domain' ), 'Our Sample Plugin' ), 'Our Sample Plugin', 'manage_options', 'our-sample-plugin', 'olin_osp_settings' );
} 

function olin_osp_settings() {
	?>
	<div class='wrapper'>
		<h1><?php _e( 'Our Sample Plugin Settings', 'our-very-unique-domain' ); ?></h1>
		<!-- Imagine that there is some really exciting functionality happening here -->
	</div>
	<?php
}
?>

The first thing we’ll need to do is initialize the localization, and it’s conceptually the exact same thing for plugins as it is for themes. In the init action that we are using within our plugin, we’ll add the load_plugin_textdomain() function.

As you can see, we’re identifying our unique textdomain as well as the location of the translation files, in this case the /languages folder within the plugin. Then, we can go about our business as usual preparing strings to be localized within our code, just as in the case of themes.

The files for Our Sample Plugin might ultimately look something like this:

sample plugin localization file structure

A word on .MO file nomenclature: In looking at the image above, you may notice that the nomenclature for our .MO files has changed within a plugin as opposed to how we labelled it in our theme.

When looking for compiled .MO translation files, WordPress looks for a different syntax for theme localizations than it does for plugin localizations. With theme localizations, you’ll want to name your .MO file in the format of locale.mo. For example, it translating your theme to German, your theme translation file in the /languages directory within your theme should be named de_DE.mo.

On the other hand, if you are localizing a plugin, WordPress will seek the translation file in your specified /languages directory within your plugin in the format of pluginname-locale.mo. In this instance, the plugin name corresponds directly to the textdomain you assigned to the localization in your plugin. Keeping with the examples we had laid out above, our translation file would thus be named our-very-unique-domain-de_DE.mo if we were translating that plugin into German.

Let’s Do Some Translation: Introducing Poedit!

Poedit is a viciously popular open-source tool that you can download and install on your computer to help you create and maintain all of the files you’ll need.

Poedit will automatically sort through all of the source code you have in your plugin or theme and return all strings that you have defined to be localizable through the _e() or __() functions. Then, it’s just a matter of going through each string and providing a translation for a specific language. Let’s get started by downloading and installing Poedit from http://www.poedit.net/download.php.

Creating a .POT file

If you are localizing your own plugin or theme, the first thing we’ll need to do it create a .POT file. To refresh your memory, a .POT file is just a .PO file that doesn’t have any definitions – it merely defines the strings that need to be translated. By default, Poedit looks for .POT files to open and work off of when a translator first seeks to localize your code, but since we don’t have one yet we’ll need to make it first. To do this, fire up Poedit, click File and then select New catalog as per the screenshot below.

Poedit

When creating a new catalog, the first thing you’ll be brought to is the Project info tab.

You can fill this in as completely as you like, but all that is really required is to give our new catalog a project name. Give it a name, and then click on the Paths tab.

Poedit

Now we need to create the path to our translation file. This path is relative to the file or files being translated.

Since I consider it best practice to create a separate directory for your language translations, I prefer adding ../ for the path as I’ve done in the screenshot below.

Once you have your path set, click the Keywords tab to continue.

Poedit

In the Keywords tab, we need only to define the GNU gettext elements that we used to prepare our strings for localization within our code. Again, __ and _e are the most common gettext functions that are used, but if you happened to use _n, _x, or any other gettext functions you will need to define them all here.

I like to remove the functions I don’t need, and so I’ll remove each of the functions I am not using, as in the screenshot below.

Poedit

This is a standard Keywords configuration that will suit the purposes of our discussion very well. We’ve added only __ and _e because these are the functions being called in our code.

Make your additions and then click OK, as per the screenshot below.

Poedit

Upon clicking OK, we’ll be prompted to save our configuration as a new .PO file.

Before you save your file, be sure to navigate to the correct location per what you set in the Paths tab of your catalog. Poedit will use the location you save your file to as a point of reference when it searches for the files you have added your localization strings to.

In our example, we’ve added a path of ../, so we’ll want to save the new .PO file in a subdirectory where your files are located. While any subdirectory will do, you might as well use something descriptive like /languages or /lang. Save your .PO file with a name appropriate to your purpose.

Just to be deftly creative, I went ahead and used appropriate-name.po.

Poedit

Upon saving the file, Poedit uses the path parameter that you set to find and index any files it sees. At this point, Poedit checks all of the files available against the list of gettext functions you defined in the Keywords tab and returns a list of translatable strings to you.

Our example below shows a short list of strings revolving around my dog and I. Click OK, and you have a blank .PO file with a few string definitions.

Poedit

This is the tricky part – and essential for creating a .POT file with Poedit.

Before you do anything else here, save the file a second time.

The reason this is necessary is that when Poedit initially creates the .PO file, it saves the file first and then imports the translatable strings. If you fail to save the file a second time and close it, you’ll end up with a blank .PO file that has no translatable strings, thus defeating the purpose.

Once you save your .PO file a second time (with your translatable strings added), close the file and quit Poedit. Don’t worry – we’ll be right back.

Poedit

Now navigate to your directory structure where you have saved your .PO file. You’ll note that you actually have two files available: a .PO file that you created, and a .MO file that Poedit automatically compiled for you when you saved the .PO file.

Because we first want to create a template file, we don’t need the .MO file – go ahead and delete it. Then, just rename the .PO file to a .POT file.

When you’re done, you’ll end up with just one .POT file in your /languages directory as shown in the screenshot below.

Poedit

Now we’ve got our .POT file, so let’s start translating!

If all you are aiming to do set up your theme or plugin so that it can be easily localized by others, congratulations! At this point, you are all set, and you can move on with your life! So long as you include your shiny new .POT file in the correct directory, anybody will be able to work with and translate your theme or plugin into an infinite number of languages.

That said, let’s assume you want to actually do a few translations. Get started by firing Poedit back up and instead of selecting New catalog, select New catalog from POT file.

Poedit

Go ahead and find the .POT file you just created, open it, and then click OK to the subsequent catalog settings (they are the exact same ones you set yourself). Poedit will then ask you what you’d like to save your new .PO file as.

Give it an appropriate name and save it as shown in the screenshot below.

Poedit

It’s really clear sailing from here on.

Just click on the string you’d like to translate in Poedit and type a translation into the box on the bottom of the Poedit window as shown in the image below.

Poedit

We’ll wash-rinse-and-repeat this procedure until all of the strings have been translated. Editing strings is just as easy… just click on the the string you want to edit and make your modifications.

Remember, each time you save a .PO file, a new .MO file will be compiled for you. It is the .MO file – not the .PO file – that WordPress will actually use when doing translations.

Poedit

Localization sounds all well and good, but there isn’t, by chance, an easier way to do this, is there?

WPML

Funny you should ask that. There’s a pretty cool tool that’s been in development for some time now called WPML (available at wpml.org). It’s a premium plugin and well worth the investment.

WPML greatly reduces the amount of time and effort it takes to localize your theme or plugin, at least within the context of a single website. All you need to do is identify your localization strings properly within your plugin or your theme by wrapping them in __() or _e() tags. Once you make your string identifications, WPML takes over, eliminating the hassle of setting up .PO, .MO and .POT files and sorting out how to add them to your files and themes.

When you initially run WPML on your WordPress site, WPML acts like Poedit and scans through all of your theme and plugin files in search of translatable strings. Upon creating a list of strings, WPML asks the user to define which languages the website should be translatable to and automatically creates the necessary .PO and .MO files needed to support each.

After that, WPML provides a really slick interface inside the WordPress backend that gives you options to translate strings as well as whole posts and pages. Cooler yet, it also creates unique, translated permalinks so that your posts and pages can be indexed in multiple languages by default!

Finally, WPML also integrates a translator management system. This lets you hire professional translators to do translation work directly on your site. Alternatively, you can use their management system to assign specific members of your own staff to do translation in a particular language.

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.

  • Helen Natasha Moore

    Wow. I love the way SitePoint comes up with really obscure stuff I would never know about! However, I had to read some way through it before I knew whether I’d be interested in this article or not. I’d never heard of ‘I18n,’ I wondered whether ‘localized’ meant ‘on your own machine, not on the server’ and there was no link to the previous article which I somehow missed, despite reading all SitePoint tweets. Good work tho! :-)

  • http://www.onsman.com Ricky Onsman

    Sorry about the missing link, Helen. I’ve added one to the first sentence of this article.

  • nightS

    Internationalize and localize your WP theme and add an rtl.css (for RTL support) and you’ll have a super perfect WP theme =)
    Great Tutorial!

  • RavanH

    Hi Rick Onsman, I must say, an impressively comprehensive piece on the inner workings plus (important) the work involved in creating actual translations :)
    Two remarks:
    1. An unexplained piece of code (in your functions.php example) is left ‘hanging’ without an explanation of its possible uses:

    $locale_file = TEMPLATEPATH.”/languages/$locale.php”;
    if ( is_readable($locale_file) )
    require_once($locale_file);

    2. Without wanting to bash WPML (like a relatively steep learning curve, too much options for the average multi-lingual blogger and the incompatibility with WordPress in Multi-site mode) I would like to suggest another language plugin — less complicated, free, and actively developed to date — that allows for multi-lingual blogging: qTranslate

    • Mick Olinik

      The piece of code you are referring to is really just a safety function that allows the function to die gracefully if there is an issue with the locale file.
      Your thought on qTranslate is totally valid, and I use it a lot. Really just an oversight in not adding it to the article… nice addition! :)

  • bluesix

    I’m curious why the unique domain parameter is passed for each _e() call. Wouldn’t it be more efficient if it was a global? I can’t think of an instance when you’d want different localisations on a single page.

  • Eva

    The first screenshot under the headline “Creating a .POT file” has the selection “New catalog from POT file…” highlighted rather than “New catalog…” While it is not off significantly, it might be confusing to some especially given that the sentence right before it says, “… select New catalog as per the screenshot below.”
    Having said that, thank you for a very informative and useful article. I will be adding other languages to my sites.