Localizing WordPress Themes and Plugins

What WordPress provides

Look through almost any of the core WordPress files and you will see that it is littered with English:

menu[0] = array(__('Dashboard'), 'read', 'index.php');

if (strpos($_SERVER['REQUEST_URI'], 'edit-pages.php') !== false)
	$menu[5] = array(__('Write'), 'edit_pages', 'page-new.php');
	$menu[5] = array(__('Write'), 'edit_posts', 'post-new.php');
if (strpos($_SERVER['REQUEST_URI'], 'page-new.php') !== false)
	$menu[10] = array(__('Manage'), 'edit_pages', 'edit-pages.php');
	$menu[10] = array(__('Manage'), 'edit_posts', 'edit.php');

At first glance this would suggest that to get WordPress in another language will require a special version of the code directly translated for that language. This sounds like a lot of work! Fortunately this is not the case, and 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[5] = array(__('Write'), 'edit_pages', 'page-new.php');

Did you notice that surrounding the text ‘Write‘ is the strange looking __ ()? This 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.

#: wp-admin/menu.php:10
msgid "Write"
msgstr ""

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:

#: wp-admin/menu.php:10
msgid "Write"
msgstr "Ecrire"

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:

// ** MySQL settings ** //
define('DB_NAME', 'putyourdbnamehere');    // The name of the database
define('DB_USER', 'usernamehere');     // Your MySQL username
define('DB_PASSWORD', 'yourpasswordhere'); // ...and password
define('DB_HOST', 'localhost');    // 99% chance you won't need to change
define('DB_CHARSET', 'utf8');
define('DB_COLLATE', '');

// You can have multiple installations in one database if you give each a
$table_prefix  = 'wp_';   // Only numbers, letters, and underscores please!

// Change this to localize WordPress.  A corresponding MO file for the
// chosen language must be installed to wp-content/languages.
// For example, install de.mo to wp-content/languages set WPLANG to 'de'
// to enable German language support.
define ('WPLANG', '');

/* That's all, stop editing! Happy blogging. */

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:

Wordpress In Chinese

48 thoughts on “Localizing WordPress Themes and Plugins”

  1. This is a great tutorial! After reading it I’m considering to internationalize my blog theme.

    However, the instructions about load_plugin_textdomain are incompatible with Gengo, a Compatibility Page:

    Just like the code that adds widget-capabilities to plugins, calls to load_plugin_textdomain cannot be made immediately. Plugins must call load_plugin_textdomain inside a function that runs on the ‘init’ hook, or at the earliest, the ‘plugins_loaded’ hook. Plugins that do not do this are coded incorrectly, according to advice from WordPress core developers.

  2. Hi Leonardo, I’ve updated the guide to reflect this. While it may cause incompatibility with Gengo, the method is only a suggestion by the WordPress developers and not a requirement (according to the Codex). Still, it is better to show the ideal method!

  3. I managed to create a pot file (this step is missing in the tutorial), translated it and compiled the po file into a mo file. "F jS, Y" is translated as "jS \\d\\e F \\d\\e Y" and I get times like "5th de April de 2007". PHP or WordPress aren’t translating "5th" to "5º" and "April" to "abril". I did set my browser and wp-config.php to pt_BR. What else should I do to get "5º de abril de 2007"?

  4. Leonardo,

    Producing POT files is covered in Translating WordPress Themes & Plugins, this article is just concerned with how to put the appropriate PHP code into a theme or plugin.

    How are you displaying the date in your code? For WordPress to replace the months you must be showing the date through a WordPress function (and not just a PHP function). I’m not sure that PHP or WordPress would convert ‘5th’ to ‘5º’

    %d comment should be correct!

  5. Hello, John!

    Too bad I found out that article only after commenting here (and googling for the solution). It explains the procedure very well, thanks!

    About the time format, I’m using (get_)the_time. If there’s better fitting solution, please let me know! Ideally, I’d use a function which already knows which time formats are used in any locale.

    On %d and %, I don’t know why, but only % worked for me. From my background as a free software translator, I’m used to %d, %s and even {} but never knew about a %.

  6. If you are using that function then WordPress should convert months. Is the locale actually being loaded? Does the locale translate the month strings?

  7. In fact, WordPress does translate month names correctly in my blog, but not in my local test site (so that’s my fault). But the "th" isn’t properly translated, and neither does WordPress seem know the proper date format for each locale. Is there anything I can do about the two last issues?

  8. Hello, that’s me again. I’m having a hard time with the printf, because many template tags display the result directly, instead of returning to printf. In example:

    '.comment_author_link().''); ?>

    In this line, we get comment_author_link then ' Says:'

  9. I’ll place some extra spaces to preserve the code:
    < ?php printf (__ ('%s Says:', 'aqLite'), ''.comment_author_link().'te>'); ? >
    < ?php comment_author_link() <ci te></ci te> Says: ? >

  10. Leonard,

    You were correct about the __ngettext example! The reason is that the % is being passed into a special WordPress function, not printf. The WordPress function requires % and not %d.

    I don’t believe WordPress translates the ordinal suffix at all, and even PHP itself says that the ‘S’ date modifier is for English suffixes only. It looks like you’ll have to invent your own method to work around this!

    Most WordPress functions have two versions – one that echos and one that returns. If you using the data inside printf then you need the return version. In the case of comment_author_link you will need to use get_comment_author_link.

  11. About the suffix, I really can live without it. In Brazil we use it only for the first day of the month, even if (as far as I know) it’s not a formal rule.

    Thank you for your better-than-codex guidance! I’ll try it as soon as I get some more time for the WordPress theme, and then I’ll give you some feedback.

  12. Hello again, Jonh!

    I’m making progress on the theme i18n, and it should be released soon. The almost universal get_ tips fixed most of my issues!

    Unfortunately, there’ no get_comments_rss_link; and get_comment_type doesn’t let us specify the strings for comment, pingback and trackback as comment_type does. I just discovered this functions (in WordPress source code) are quite simple, so I created similar ones to match my needs.

    I also made sprintf and __ngettext work together. I’ll send you example code, as I believe the article could show it. I’ll use the contact form, because the comment form restricts code.

  13. Thank you so much for sharing this. I would be grateful if you can cover also the bi-di languages (Arabic, Urdu, Farsi, Hebrew, etc) and how to flip and reverse the whole layout to be right to left. That would be great and I think it will add a lot to your efforts to reach broader audience.

    Thanks again

  14. John,

    Thanks for the article–it got me started nicely on internationalising my plugins–but I very quickly hit a brick wall. I can’t get xgettext to work on my Windows development machine. It doesn’t seem to recognise PHP files at all. Someone told me the win versions were rather out of date. Do you know of any ways round this road-block? Thanks for your help.

  15. A great tutorial indeed, thank you so much!. I’m using it for internationalization of the excellent TMA theme (The Morning After). Sadly, the tutorial doesn’t seem to explain the ngettext thingie right. In TMA a similar function (comments_popup_link) is often called, but when I change the relevant passage the way you suggest, it gets messed up. Precisely, I change


    When there is one comment or two – all gets translated just fine. But poedit doesn’t see the other possibilities (3 comments, 4 comments, and so on) and where there are three comments or more – no text is visible at all. Any ideas?

  16. BTW, the xhtml code function doesn’t seem to work on your site, contrary to what a tooltip above the comment area says.

  17. Bah! I sorted it out finally. For some strange reason PoEdit couldn’t handle my language settings properly and when I __ngettex the %, it returned errors and added some strange referral to #, php-format file, even though there’s no such file in the theme I’m translating (and returned an error when saving).

    However, I finally decided to do it the hard way, opened the .po file in Notepad ++, then added the following “by hand”

    #: home.php:42
    #: single.php:44
    msgid “% comment”
    msgid_plural “%d comments”
    msgstr[0] “0 komentarz”
    msgstr[1] “% komentarze”
    msgstr[2] “% komentarzy”

    Then I reloaded the file in PoEdit (but without reloading the files from the folder) and saved – without any problems this time. Just don’t trust PoEdit and you’ll be fine 🙂

  18. Great article, was very helpful for localizing a wordpress theme! Unfortunately, there are some typos in the code examples (spaces before brackets), that took some extra time to find and correct to get the code working. You might want to correct those. Peace.

  19. Hello there,
    I need your help on this topic.
    I’ve already translated the whole theme (Arras 1.4) and MOST of the content, when i load the page on the second language, is translated.
    I say most because some words like: “Featured Stories”, “Latest Headlines” and the “Home” menu link remain untouched, which is kinda confusing because i translated them as well, just like all other strings.

    Can anyone help me?
    thx in advance,

  20. I understand how to mark my theme and what the role of the pot, po and mo files is – in theory. In reality, the article says to run “a localization tool” to generate them. Great. Care to tell us what “a tool” is?

    I also read the WordPress Codex pages and they refer to a GNU gettext tool. Had a look at their site http://www.gnu.org/software/gettext/ and it’s clear they went to great lengths to make it as confusing as possible for normal people to get this to work.

    I’m not a GNU/Linux/SVN/computer science expert. I’m just a blogger with basic programming skills, want to localize my blog and move on with my live. Can someone please explain in “idiot English” what tools I need and how to run them? Thanks, I’d really appreciate it!

  21. Hi John, nice article, just the Czech language example is not quite right, it should be:
    0 oken
    1 okno
    2 okna

    in fact, it is:
    0, 5, 6, 7, 8, 9, 10, 11… oken
    1 okno
    2, 3, 4 okna

    btw this is only one of many types of declension (there are 4 types for neutral, 4 types for feminine and 6 types for masculine nouns, each declension has 7 cases for singular and 7 cases for plural from) – FUN! – no wonder most Czech people are unable to use correct Czech.