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

    Mick Olinik
    Share

    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:

    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: 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:

    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.