Localizing WordPress Themes and Plugins
Thought Internet is mainly English-speaking, there is a word of speakers outside of English. Not exactly everyone reads and writes English at the same level, or non-native speakers wouldn’t want software in their own language.
This tutorial will show how to prepare WordPress themes and plugins for localization. It will contribute to the international reach of WordPress, and you’ll receive the very satisfying feeling of seeing your own work in another language.
Keep in mind that:
- You do not need to speak multiple languages yourself. Localizing a theme or plugin just requires modifications to your code, not actual translation of words;
- The localization process is relatively simple. You can localize everything you create!
The tutorial points to beginners, but it will take you through some fairly advanced concepts. It is assumed that you are familiar with both HTML and PHP. Although detailed explanations will be given at each step.
What WordPress provides
Look through almost any of the core WordPress files and you will see that it is littered with English:
menu = array(__(‘Dashboard’), ‘read’, ‘index.php’);
if (strpos($_SERVER['REQUEST_URI'], ‘edit-pages.php’) !== false)
$menu = array(__(‘Write’), ‘edit_pages’, ‘page-new.php’);
$menu = array(__(‘Write’), ‘edit_posts’, ‘post-new.php’);
if (strpos($_SERVER['REQUEST_URI'], ‘page-new.php’) !== false)
$menu = array(__(‘Manage’), ‘edit_pages’, ‘edit-pages.php’);
$menu = array(__(‘Manage’), ‘edit_posts’, ‘edit.php’);
You may suggest that to get WordPress in another language you will need a special version of the code directly translated for that language. Fortunately WordPress makes use of a clever localization system called GNU gettext. This is a very stable and widely used framework that allows pieces of text to be ‘tagged’ as translatable, and then extracted into a dictionary where a lookup can made. Before we get too far into the specifics, let’s look at how text is tagged by taking a line from the above code:
$menu = array(__(‘Write’), ‘edit_pages’, ‘page-new.php’);
Here, surrounding the text ‘Write’ is the strange looking __ () is actually a special WordPress function that takes the text content (whatever is inside the brackets) and looks for a translated version. If a translation can be found it is used instead of the original. If no translation can be found then the original text is used.
WordPress provides two such localization functions:
- __($text) – Looks for a translated version of $text and returns the result
- _e($text) – Looks for a translated version of $text and echo the result to the screen (i.e. effectively it is echo __($text))
The basic premise of localization is that you mark all your displayable text with these special functions. This applies to both themes and plugins. Once marked you then run a localization tool over the code which will extract out all marked text into a template file. In the GNU gettext world this is called a POT file – Portable Object Template.
The POT file is simply a text file containing all the marked pieces of text, but organized in a special format.
When you have a POT file a translator can then add translations. This can be done by using a text editor, or by using a specialized localization tool. The role of the translator is to provide a translation for every marked string:
Notice how you write your code in your native language (in this case English) and the file has an entry for each item of text with a translation into the localized language. This is effectively a dictionary, and in the GNU gettext world is called a PO file (Portable Object). The special WordPress functions __() and _e() take a piece of text and look through this ‘dictionary’, returning any translation that matches. If no translation matches then the original text is returned.
There is a final stage which involves ‘compiling’ the text PO file into a high-speed binary file called a MO (Machine Object) file. This makes the dictionary lookup process much faster.
To recap this chapter we’ve discovered that WordPress uses a standard localization framework called GNU gettext. The process involved in localizing a theme or plugin is:
- Prepare the file by tagging all text (wrapping the text in the functions __() or _e())
- Extract a localization template file (POT) from the file
- Translate the template file into a specific locale (resulting in a PO file)
- Compile the PO file to produce a high-speed MO file
Configuring the WordPress locale
WordPress supports localizations for the core code, plugins, and themes. Each localization is separate from the other. For example, if you use the French WordPress localization then all the core text will be in French, but your theme and plugins will not be. If you want everything in French then you must find a localization for the theme and each plugin.
Language files for WordPress can be downloaded from WordPress In Your Own Language. A downloaded language file should consist of a .mo file (possibly zipped) that should be placed in the WordPress language directory (wp-includes/languages/). For example, downloading and installing the Chinese localization would result in this file being store:
By default, WordPress will always display text in US English, unless configured otherwise. To change this you must edit the file wp-config.php. Located in this file is a WPLANG setting:
2. // ** MySQL settings ** //
3. define(‘DB_NAME’, ‘putyourdbnamehere’); // The name of the database
4. define(‘DB_USER’, ‘usernamehere’); // Your MySQL username
5. define(‘DB_PASSWORD’, ‘yourpasswordhere’); // …and password
6. define(‘DB_HOST’, ‘localhost’); // 99% chance you won’t need to change
7. define(‘DB_CHARSET’, ‘utf8′);
8. define(‘DB_COLLATE’, ”);
10. // You can have multiple installations in one database if you give each a
11. $table_prefix = ‘wp_’; // Only numbers, letters, and underscores please!
13. // Change this to localize WordPress. A corresponding MO file for the
14. // chosen language must be installed to wp-content/languages.
15. // For example, install de.mo to wp-content/languages set WPLANG to ‘de’
16. // to enable German language support.
17. define (‘WPLANG’, ”);
19. /* That’s all, stop editing! Happy blogging. */
21. define(‘ABSPATH’, dirname(__FILE__).’/');
Change this to reflect your chosen locale (without any file extension):
define (‘WPLANG’, ‘zh_CN’);
The next time you view your site the core of WordPress should be in your chosen locale.
Now we get to the main idea of this tutorial: localizing a theme or plugin. To demonstrate the techniques involved we will look at a file from the default WordPress theme, which is surprisingly not localized. The file is:
The exact same process involved in localizing a theme applies to plugins.
The first step in preparing your theme or plugin is to decide on a text domain. A text domain is a way for you to separate your localized messages from localized messages in other themes and plugins, and from the rest of WordPress itself. A text domain is simply a text string that identifies your theme or plugin, and is typically the name of the theme or plugin (without any .php extension). Using the default theme as an example, we will chose a text domain of kubrick. Once we’ve chosen the domain we need to tell WordPress about it.
For plugins we insert the following function in an appropriate part of the plugin file and before any text is output:
Ideally this will be inserted after all other plugins have loaded to allow the best compatibility. This can be achieved by loading the domain in the init action.
add_action (‘init’, ‘my_plugin_init’);
function my_plugin_init ()
For themes we insert the following function at the top of functions.php in the theme directory:
Our theme or plugin is now setup to receive a localization. It is important to note that we must specify the text domain whenever we use the WordPress localization functions:
- __($text, $domain) – Looks for a translated version of $text in text domain $domainand returns the result
- _e($text, $domain) – Looks for a translated version of $text in text domain $domainand echo the result to the screen (i.e. effectively it is echo __($text))
If we do not specify the text domain then WordPress assumes the localization is contained within the core locale files.
Marking text strings
As we’ve already seen, marking a string is simply a case of wrapping it inside an appropriate WordPress function. Using the following highlighted version of index.php, we will look at individual pieces of text and explain how they should be marked.
1. <?php get_header(); ?>
3. <div id="content" class="narrowcolumn">
5. <?php if (have_posts()) : ?>
7. <?php while (have_posts()) : the_post(); ?>
9. <div class="post" id="post-<?php the_ID(); ?>">
11. <a href="<?php the_permalink() ?>" rel="bookmark"
12. title="Permanent Link to <?php the_title(); ?>">
13. <?php the_title(); ?>
17. <?php the_time(‘F jS, Y’) ?> <!– by <?php the_author() ?> –>
20. <div class="entry">
21. <?php the_content(‘Read the rest of this entry »’); ?>
24. <p class="postmetadata">
25. Posted in <?php the_category(‘, ‘) ?> |
26. <?php edit_post_link(‘Edit’, ”, ‘ | ‘); ?>
27. <?php comments_popup_link(‘No Comments »’, ’1 Comment »’,
28. ‘% Comments »’); ?>
32. <?php endwhile; ?>
34. <div class="navigation">
35. <div class="alignleft">
36. <?php next_posts_link(‘« Previous Entries’) ?>
38. <div class="alignright">
39. <?php previous_posts_link(‘Next Entries »’) ?>
43. <?php else : ?>
45. <h2 class="center">Not Found</h2>
46. <p class="center">
47. Sorry, but you are looking for something that isn’t here.
49. <?php include (TEMPLATEPATH . "/searchform.php"); ?>
51. <?php endif; ?>
55. <?php get_sidebar(); ?>
57. <?php get_footer(); ?>
First up is the title that appears when you hover over a link (from line 12):
Permanent Link to <?php the_title(); ?>
This uses the function the_title (), which displays the post title resulting in text such as ‘Permanent link to My First Post‘. Obviously we don’t want to localize every post title so we need to indicate that the post title is added at run-time. A simple first attempt at localizing this might look like:
<?php _e (‘Permanent Link to’, ‘kubrick’) ?> <?php the_title (); ?>
This would indeed produce the correct result of ‘Permanent Link to My First Post‘. However, it assumes that the localized language uses the same rules as English and that the subject ‘My First Post‘ will follow ‘Permanent Link to‘. This is not always the case and another language may need to reorder the words to look something like ‘My First Post is a permanent link‘. The translator may not actually be able to directly translate the English (the language or grammar may not support a similar concept) and could be required to dramatically change the meaning of the text, even to the point of removing the run-time title. This apparently simple line demonstrates the need to be careful about making assumptions based upon your own language.
A better attempt is:
<?php printf(__ (‘Permanent Link to %s’, ‘kubrick’), get_the_title())?>">
Here we use the PHP function printf which allows us to insert run-time data. We pass this function the localized string ‘Permanent Link to %s’, as well as the run-time value received from the function get_the_title () (remember that the_title displays the title, while get_the_title returns it to printf). The printf function will then insert the runtime title where the %s symbol is, resulting in a correct display and also allowing a translator to change the position of the run-time data in a localization:
msgid "Permanent Link to %s"
msgstr "%s is a permanent link"
Next we have the time display (line 17):
<?php the_time(‘F jS, Y’) ?>
Localizing time can be a complicated process and will be dealt with separately later. The important thing to remember here is that we need to give the translator the ability to change how the date and time is displayed as it is unlikely to be the same as in English.
<?php the_time(__ (‘F jS, Y’, ‘kubrick’)) ?>
A simple line to display the post content (from line 21):
<?php the_content(‘Read the rest of this entry »’) ?>
In general you should always include punctuation in localized strings as this allows a translator to remove it when not appropriate. In this case the » symbol is included.
<?php the_content(__(‘Read the rest of this entry »’, ‘kubrick’))?>
Next is a localization that requires some thought:
<?php comments_popup_link(‘No Comments »’,
’1 Comment »’,
‘% Comments »’); ?>
The problem here is that while in English there are three variations of the phrase (for none, one, and more than one), some languages have more or less variations. The WordPress function comments_popup_link () only allows three to be specified, so how do we work around this? The answer for the first two variations is simple: we do nothing special:
__ (‘No Comments »’, ‘kubrick’),
__ (’1 Comment »’, ‘kubrick’)
The translator is able to localize this according to their language with no ill effects if they require more or less variations. The third case is different and we need to allow additional variations. WordPress provides us with a specific function called __ngettext. Details of this function will be given in the next section
__ngettext (‘%d comment’, ‘%d comments’,
get_comments_number (), ‘kubrick’)
This results in a final localization of:
<?php comments_popup_link(__ (‘No Comments »’, ‘kubrick’),
__ (’1 Comment »’, ‘kubrick’),
__ngettext (‘% comment’, ‘% comments’,
Plurals are tricky enough in English, and you cannot assume that other languages follow the same rules. Chinese, for example, requires no additional suffixes for plurals, while Czech has very complicated rules. Fortunately WordPress has already thought of this and you can localize plurals using the special function __ngettext:
<?php __ngettext ($single, $plural, $number, $domain); ?>
This function takes three parameters (as well as the domain).
· $single – The text when $number is 1
· $plural – The text when $number is greater than 1
· $number – The actual number
In the following .PO file you can see how this would look in Czech:
msgid "%d window"
msgid_plural "%d windows"
msgstr "0 okno"
msgstr "1 okna"
msgstr "2 oken"
Note how Czech has more cases than English.
Dates and times
Dates and times are one of the most tricky aspects of localization due to the variety of words, formatting, and positioning. Let’s look at a few possibilities to show how complicated the situation is.
· US English – 4/28/2007
· UK English – 28/4/2007
· Chinese – 2005年2月27日
We can see that not only do we need to translate the words, but we also need to be able to change the position and change the formatting.
Most dates will be displayed using the PHP function date. This allows the date to be configured according to various format settings (see the PHP date page for full details). As such, any date format string should always be localized to allow a translator to modify it:
date (‘l, F js, Y’, ‘myplugin’);
However, this assumes that the host running your site will be configured in the appropriate locale. Typically this is not the case. To get around this problem WordPress provides an additional function that accepts a date format string, but internally translates any day or month strings for you.
mysql2date (__ (‘l, F js, Y’, ‘myplugin’));
Numbers and currencies
Like dates and times, numbers and currencies are also very tricky. For example:
· 4 HK$
If you are displaying a lot of numbers and it is important to have the correct separators and currency formatting then you will need to devise your own method of allowing the user to configure the output. Typically this will include:
· Number separator (1,000.50 or 1.000,50)
· Currency symbol ($, £, ¢ etc)
· Currency position (before, after, with or without space)
It should be noted that PHP provides a useful function number_format which will take a number and format it according to the parameters you specify:
number_format (1000.50, 2, ‘.’, ‘,’);
This will display the number 1000.50 to two decimal places using a period as the decimal separator and a comma to separate thousands. The output value will be 1,000.50.
Word order is not usually an issue until you start inserting multiple values at run-time. Consider the following phrase:
Today is %s and the weather is %s
This seems innocent enough. However, in another language the word order may need to be reversed. Without any additional help this would be impossible – even if we translated this to ‘The weather is %s and today is %s‘, the data that is inserted at run-time would still be in the wrong order (we would end up with ‘The weather is Sunday and today is sunny‘).
To cater for this you can use placeholders. A placeholder is similar to the existing printf-style strings, but also defines a place:
Today is %1$s and the weather is %2$s
Here we have defined that the first runtime parameter will contain the day, and the second runtime parameter will contain the weather. This can then be translated into whatever order is needed:
The weather is %2$s and today is %1$s
This time the correct run-time data will be inserted into the appropriate position.
function show_result ()
alert (‘You message was successfully received’);
First we replace the text with a string variable:
function show_result ()
Then in the HEAD section of the page we can generate the value at runtime:
var plugin_result_message = ‘<?php _e (‘You message was successfully received’, ‘plugin’); ?>’;
Summary of localization
The most important rule when localizing a file is to never assume that a localization will use the same rules as yours. If you are adding text at run-time then always give the translator the ability to change its location.