Rolling Up Your Sleeves and Getting into the Nitty Gritty of I18n in WordPress
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"
"https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="https://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" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="https://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:
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: https://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: https://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:
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.
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.
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.
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.
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.
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
.
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.
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.
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.
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.
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.
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.
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.
Localization sounds all well and good, but there isn’t, by chance, an easier way to do this, is there?
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.