How To Internationalize WordPress Themes
This page may contain links from our sponsors. Here’s how we make money.
If implemented from the start of a project, making your themes translatable is so easy, there’s just no excuse not to do it! As the owner of a theme workshop I can tell you that one of the top questions we get is “is this theme translatable” so internationalization is far from being a trivial matter. This post should cover soup-to-nuts of how to make it happen, so follow me on this journey while I show you the ins and outs of the wonderful world of Internationalization.
The Basics Of Internationalization
In a nutshell, all you are doing is replacing your hard-coded strings with functions. ‘Hello World’ becomes __( 'Hello World', 'mytranslations' )
and that’s it. This function checks if a translation exists for the first parameter; if it does, the translation is used – if it doesn’t, the first parameter is output as is. In addition to marking our strings like this we need to include a language file, indicate to WordPress that this file exists, and we’re done! The tricky part of internationalization is taking into account all the different languages. The heavy lifting has been done for us by WordPress, all we need to do is remember a couple of functions!
Internationalizing Our Code
Generally you’ll be using the two basic function, rarely a third one, and even more rarely the remaining 13 or so. Let’s look at these functions in order of how much they are used.
Basic Translation Functions
<h2><?php _e( 'Recent Posts', 'mytranslations' ) ?></h2>
The _e()
function registers a translatable string and echoes it straight away. The second parameter is the text domain. When you’re translating a theme or a plugin you should always add a text domain to separate it from other translations. We’ll be looking at text domains in a bit more detail below. If you don’t want to echo the translation straight away you can use the __()
function.
$heading_level = 1; echo ";<h' . $heading_level . '>" . __( 'Recent Posts', 'mytranslations' ) . "</h' . $heading_level . '>";
Using Placeholders
An important rule for translations is that they can not contain any variables. Whenever you mark a string for translation with any of the functions, the exact string is looked up. The function will look for “Recent Posts” in the German translation file for example, and display the German translation next to it. If you use a variable the string can not be looked up since it will always be different: “4 Comments”, “3 Comments”, “189 Comments”. Due to this, we use the sprintf()
and printf()
function to provide placeholders.
printf( __( 'Welcome back %s', 'mytranslations' ), $current_user->display_name );
This is much better! The text which is looked up will always be ‘Welcome back, %s’. The placeholder will be included in the translation and will be replaced with the proper argument when the printf()
function executes. If you have more than one placeholder in a string you should use argument swapping, which is a great technique for maximizing the flexibility of translations.
printf( __( 'Your email has been changed to %1$s and your city is now %2$s', 'mytranslations' ), $current_user->email, $current_user->city );
Since placeholders are now marked, the translator can use them in any order. This technique is especially for languages which use a different word order than the one you’re coding for. The code below will work fine, even though the placeholders have changed places while the arguments have not:
printf( __( 'Your city is now %2$s and your email has been changed to %1$s', 'mytranslations' ), $current_user->email, $current_user->city );
The printf()
family of functions is extremely powerful. You will mostly be using ‘%s’ and ‘%d’ as placeholders (for strings and integers) but there are 15 in total. The documentation for sprintf()
is a good place to start reading about these functions.
Plural Forms
Plural forms are a bit more difficult because not only is there a variable in the string but a part of the string is different, based on that variable. This is where the handy _n()
function comes in
printf( _n( 'You have %d unread message', 'You have %d unread messages', $count, 'mytranslations' ), $count );
As you can see from above the _n()
function has four parameters. The singular form, the plural form, the number based on which the singular or plural will be displayed and the text domain
Indicating Context
In a handful of cases translatable strings might not be clear. In the following example, the translator might not be able to translate correctly:
_e( 'Comment', 'mytranslations' );
The problem arises from the multiple meanings of the word ‘comment’. It can be a noun indicating a comment which has been made, or it can be a verb which indicates the action of commenting. In my native tongue – Hungarian – these two words are different; it would look might strange indeed if they were mixed up. Luckily, using the function _x()
we can account for context sensitive words.
$comment_text = _x( 'Comment', 'verb', 'mytranslations' );
This method will present translators with the context as well, making sure that they have all the information they need to create a perfect translation. In addition to the _x()
function you can use _ex()
to echo the string straight away, or the _nx()
function to combine contexts and plurals.
_ex( 'Message', 'noun', 'mytranslations' );
This example above shows how you can echo a context sensitive translation. The example below shows how you can create singular and plural versions of context sensitive strings.
sprintf( _nx( '%d Like', '%d Likes ', $like_count, 'noun', 'mytranslations' ) );
Escaping and Translations
In many cases you will need to escape characters to make sure you can use your translated strings in HTML or in attributes. WordPress has 6 functions for you which follow the scheme of what we’ve discussed above.
esc_attr__( $text, $domain )
Retrieves and returns the translation of$text
and escapes it for use in attributes.esc_attr_e( $text, $domain )
Retrieves and echoes the translation of$text
and escapes it for use in attributes.esc_attr_x( $text, $context, $domain )
Retrieves the translation of$text
according to$context
and escapes it for use in attributes.esc_html__( $text, $domain )
Retrieves and returns the translation of$text
and escapes it for use in HTML.esc_html_e( $text, $domain )
Retrieves and echoes the translation of$text
and escapes it for use in HTML.esc_html_x( $text, $context, $domain )
Retrieves the translation of$text
according to$context
and escapes it for use in HTML.
The Separation Of Plurals
To take a look at the remaining three functions we need to go back to plurals. Plurals reserve special stature because they’re the only type of translations which require something to be calculated at the time of output. Due to this property they can not easily be separated from the rest of the content. They need to be placed exactly where the translation takes place; they can’t be referenced. Let’s look at an example:
$count = get_message_count(); $message_text = sprintf( _n( 'You have %d unread message', 'You have %d unread messages', $count, 'mytranslations' ), $count );
It is obvious from this example that we can’t just separate out the translation and put it in a separate file somewhere. It depends on the value of $count
being calculated. The _n_noop()
and translate_nooped_plural()
functions come to our aid! Let’s rewrite the above example using these functions:
$message_plural = _n_noop( 'You have %d unread message', 'You have %d unread messages', 'mytranslations' ); $count = get_message_count(); $message_text = sprintf( translate_nooped_plural( $message_plural, $count ) , $count );
With this little trick we have made our translation independent of the value of count. Note that this does not mean that the final output is independent, it just means that the translatable text is, which was our goal. The final missing function is _nx_noop()
which – you guessed it – is the same as the function above with an added context.
$message_plural = _nx_noop( '%d Like', '%d Likes', 'noun', 'mytranslations' ); $count = get_message_count(); $message_text = sprintf( translate_nooped_plural( $message_plural, $count ) , $count );
Adding Additional Information
While not a function, you can add additional comments for translators using regular PHP comments. Especially useful for dates and other obscure strings, simply enter a comment right before the translatable string.
/* translators: Post date format, see http://php.net/date */ $post_date_format = __( 'jS, F, Y' );
Translating Javascript
Translating Javascript requires some additions to our existing workflow above. WordPress offers an excellent tool for the job, the wp_localize_script()
function. Once a script is enqueued, internationalization can be added with this function:
wp_localize_script( 'postactions', 'l10n', array( 'post_deleted' => __( 'Post successfully deleted', 'mytranslations' ), ) );
Once a script has been localized like this you can use the object named in the second parameter and its members defined in the array to use the translations.
$('.action-message').html( l10n.post_deleted );
Preparing For Localization
Now that all the code is internationalized we need to make sure that translators have a collection of strings to translate and that translations are actually tied to the theme.
Generating Translation Files
Generating translation files will require the wordpress-i18n tools which you’ll need to check out from the repository. Let’s take a look at this step-by-step.
Note: To continue, you will need the svn command line tool or an application which can handle SVN repositories and some knowledge about svn. For more information on using SVN take a look at our section on SVN before moving on.
Your first step will be to check out the repository. using the command line or your SVN application pull the following repository:
http://i18n.svn.wordpress.org/tools/trunk/
Once done you should end up with a directory of php files which you can use to gather translation data. To create a pot file all you need to do is a run a command like this:
php makepot.php wp-theme path-to-your-theme
After a few seconds of thinking the task will complete and you will see a new pot file in the directory of the language tools. The name of the pot file will correspond to the name of your theme. This file can be placed anywhere inside your theme directory but putting it into a dedicated ‘languages’ folder is advised.
Tying Translations To Themes
To make sure any completed translations can be used we need to tell WordPress where they are. This is a simple and straightforward process, something you can just drop into your functions file across projects.
load_theme_textdomain( 'mytranslations', get_template_directory() . '/languages' ); $locale = get_locale(); $locale_file = get_template_directory() . "/languages/$locale.php"; if ( is_readable($locale_file) ) { require_once($locale_file); }
The snippet above will load the correct language file if it exists. If it doesn’t the original language will be used.
Conclusion
While it may take some getting used to, I now find it off-putting if I have to hard-code some text – it becomes second nature pretty quickly. Once all the steps have been completed, buyers, translators and clients can come in and easily translate themselves using the pot files, or a plugin like WPML.