What is internationalization? #

Internationalization is the process of developing your theme so it can easily be translated into other languages. Internationalization is often abbreviated as i18n (because there are 18 letters between the i and the n).

Top ↑

Why is internationalization important? #

WordPress is used all over the world, it is a good idea to prepare WordPress themes so that it can be easily translated into other languages. As a developer, you may not have an easy time providing localizations for all your users however, any translator can successfully localize the theme without needing to modify the source code itself.

Top ↑

How to internationalize your theme? #

In order to make a string translatable in your application you have to wrap the original strings in a call to one of a set of special functions.

Introduction to Gettext #

WordPress uses the gettext libraries and tools for i18n. Note that if you look online, you’ll see the _() function which refers to the native PHP gettext-compliant translation function, but instead with WordPress you should use the __() WordPress defined PHP function. If you want to get a broader and deeper view of gettext, we recommend you read the gettext online manual.

Top ↑

Text Domains #

You need to use a text domain to denote all text belonging to that theme. The text domain is a unique identifier, which makes sure WordPress can distinguish between all loaded translations. This increases portability and plays better with already existing WordPress tools. The text domain must match the slug of the theme. If your theme’s name My Theme is defined in the style.css or it is contained in a folder called my-theme the domain name should be my-theme. The text domain name must use dashes and not underscores.

String example:

__( $text, $text_domain );

 

__( 'String(text to be internationalized)', 'text-domain' );

Change “text-domain” to the slug of your theme.

The text domain also needs to be added to the style.css header. WordPress uses it to internationalize your theme meta-data even when the theme is disabled. The text domain should be same as the one used when loading the text domain.

Header example:

/*
* Theme Name: My Theme
* Author: Theme Author
* Text Domain: my-theme
*/

Top ↑

Domain Path #

The domain path is used so that WordPress knows where to find the translation when the theme is disabled. Only useful if the translations are located in a language folder that is named something other than languages. For example, if .mo files are located in the locale folder then Domain Path will be /languages and must have the first slash. Defaults to the languages folder in the theme.

Header example:

/*
* Theme Name: My Theme
* Author: Theme Author
* Text Domain: my-theme
* Domain Path: /languages
*/

Top ↑

Basic strings #

The most commonly used one is __(). It just returns the translation of its argument:

__( 'Blog Options', 'my-theme' );

Another simple one is _e(), which outputs the translation of its argument. Instead of writing:

echo __( 'WordPress is the best!', 'my-theme' );

you can use the shorter:

_e( 'WordPress is the best!', 'my-theme' );

Top ↑

Variables #

If you are using variables in strings like in the example below you would use placeholders.

echo 'Your city is $city.'

The solution is to use the printf family of functions. Especially helpful are printf and sprintf. Here is what the right solution looks like:

printf(
__( 'Your city is %s.', 'my-theme' ),
$city
);

Notice that here the string for translation is just the template "Your city is %s.", which is the same both in the source and at run-time.

If you have more than one placeholder in a string, it is recommended that you use argument swapping. In this case, single quotes (') are mandatory : double quotes (") will tell php to interpret the $s as the s variable, which is not what we want.

printf(
__( 'Your city is %1$s, and your zip code is %2$s.', 'my-theme' ),
$city,
$zipcode
);

Here the zip code is being displayed after the city name. In some languages displaying the zip code and city in opposite order would be more appropriate. Using %s prefix in the above example, allows for such a case. A translation can thereby be written:

printf(
__( 'Your zip code is %2$s, and your city is %1$s.', 'my-theme' ),
$city,
$zipcode
);

Important! This code is incorrect.

// This is incorrect do not use.
_e( 'Your city is $city.', 'my-theme' );

The strings for translation are extracted from the sources, so the translators will get this phrase to translate: "Your city is $city.".

However in the application _e will be called with an argument like "Your city is London." and gettext won’t find a suitable translation of this one and will return its argument: "Your city is London.". Unfortunately, it isn’t translated correctly.

Top ↑

Plurals #

Basic Pluralization

If you have string that changes when the number of items changes. In English you have "One comment" and "Two comments". In other languages you can have multiple plural forms. To handle this in WordPress you can use the _n() function.

printf(
_n(
'One comment',
'%s comments',
get_comments_number(),
'my-theme'
),
number_format_i18n( get_comments_number() )
);

_n() accepts 4 arguments:

  • singular – the singular form of the string
  • plural – the plural form of the string
  • count – the number of objects, which will determine whether the singular or the plural form should be returned (there are languages, which have far more than 2 forms)
  • text domain – the theme’s text domain

The return value of the functions is the correct translated form, corresponding to the given count.

Pluralization done later

You first set the plural strings with _n_noop() or _nx_noop().

$comments_plural = _n_noop(
'One comment.',
'%s comments.'
);

Then at a later point in the code you can use translate_nooped_plural() to load the strings.

printf(
translate_nooped_plural(
$comments_plural,
get_comments_number(),
'my-theme'
),
number_format_i18n( get_comments_number() )
);

Top ↑

Disambiguation by context #

Sometimes one term is used in several contexts and although it is one and the same word in English it has to be translated differently in other languages. For example the word Post can be used both as a verb "Click here to post your comment" and as a noun "Edit this post". In such cases the _x() or _ex() function should be used. It is similar to __() and _e(), but it has an additional argument — the context:

_x( 'Post', 'noun', 'my-theme' );
_x( 'Post', 'verb', 'my-theme' );

Using this method in both cases we will get the string Comment for the original version, but the translators will see two Comment strings for translation, each in the different contexts.

Note that similarly to __(), _x() has an echo version: _ex(). The previous example could be written as:

_ex( 'Post', 'noun', 'my-theme' );
_ex( 'Post', 'verb', 'my-theme' );

Use whichever you feel enhances legibility and ease-of-coding.

Top ↑

Descriptions #

So that translators know how to translate a string like __( 'g:i:s a' ) you can add a clarifying comment in the source code. It has to start with the words translators: and to be the last PHP comment before the gettext call. Here is an example:

/* translators: draft saved date format, see http://php.net/date */
$saved_date_format = __( 'g:i:s a' );

Top ↑

Newline characters #

Gettext doesn’t like \r (ASCII code: 13) in translatable strings, so please avoid it and use \n instead.

Top ↑

Empty strings #

The empty string is reserved for internal Gettext usage and you must not try to internationalize the empty string. It also doesn’t make any sense, because the translators won’t see any context.

If you have a valid use-case to internationalize an empty string, add context to both help translators and be in peace with the Gettext system.

Top ↑

Handling JavaScript files #

Use wp_localize_script() to add translated strings or other server-side data to a previously enqueued script.

Top ↑

Escaping strings #

It is good to escape all of your strings, this way the translators cannot run malicious code. There are a few escape functions that are integrated with internationalisation functions.

Top ↑

Localization functions #

Basic functions

__()
_e()
_x()
_ex()
_n()
_nx()
_n_noop()
_nx_noop()
translate_nooped_plural()

Escape functions

esc_html__()
esc_html_e()
esc_html_x()
esc_attr__()
esc_attr_e()
esc_attr_x()

Date and number functions

number_format_i18n()
date_i18n()

Top ↑

Best Practices for writing strings #

Here are the best practices for writing strings

  • Use decent English style – minimize slang and abbreviations.
  • Use entire sentences – in most languages word order is different than that in English.
  • Split at paragraphs – merge related sentences, but do not include a whole page of text in one string.
  • Do not leave leading or trailing whitespace in a translatable phrase.
  • Assume strings can double in length when translated
  • Avoid unusual markup and unusual control characters – do not include tags that surround your text
  • Do not put unnecessary HTML markup into the translated string
  • Do not leave URLs for translation, unless they could have a version in another language.
  • Add the variables as placeholders to the string as in some languages the placeholders change position.
    printf(
    __( 'Search results for: %s', 'my-theme' ),
    get_search_query()
    );
    
  • Use format strings instead of string concatenation – translate phrases and not words -
    printf(
    __( 'Your city is %1$s, and your zip code is %2$s.', 'my-theme' ),
    $city,
    $zipcode
    );
    

    is always better than

    __( 'Your city is ', 'my-theme' ) . $city . __( ', and your zip code is ', 'my-theme' ) . $zipcode;
    
  • Try to use the same words and same symbols so not multiple strings needs to be translated e.g.__( 'Posts:', 'my-theme' ); and __( 'Posts', 'my-theme' );

Top ↑

Add Text Domain to strings #

You must add your Text domain as an argument to every __(), _e() and __n() gettext call, otherwise your translations won’t work.

Examples:

  • __( 'Post' ) should become __( 'Post', 'my-theme' )
  • _e( 'Post' ) should become _e( 'Post', 'my-theme' )
  • _n( 'One post', '%s posts', $count ) should become _n( 'One post', '%s posts', $count, 'my-theme' )

Adding the text domain by hand can be a burden if not done continusly when writing code and that’s why you can do it automatically:

  • Download the add-textdomain.php script to the folder where the file is you want to add the text domain
  • In command line move to the directory where the file is
  • Run this command to create a new file with the text domain added
php add-textdomain.php my-theme index.php > new-index.php
  • If you wish to have the add-textdomain.php in a different folder you just need to define the location in the command.
php \path\to\add-textdomain.php my-theme index.php > new-index.php
  • Use this command if you don’t want a new file outputted.
php add-textdomain.php -i my-theme

After it’s done, the text domain will be added to the end of all gettext calls in the file. If there is an existing text domain it will not be replaced.

Top ↑

Loading Text Domain #

You need to load the MO file with your theme’s translations. You can load them by calling the function load_theme_textdomain(). This call loades {locale}.mo from your theme’s base directory or {text-domain}-{locale}.mo from the WordPress theme language folder. The locale is the language code and/or country code you defined in the constant WPLANG in the file wp-config.php. For example, the locale for German is de_DE. So the code need for wp-config.php would be define ('WPLANG', 'de_DE');. For more information about language and country codes, see WordPress in Your Language.

Watch Out

  • Name your MO file as {locale}.mo (e.g. de_DE.po & de_DE.mo) if adding the the translation to the theme folder.
  • Name your MO file as {text-domain}-{locale}.mo (e.g my-theme-de_DE.po & my-theme-de_DE.mo) if you are adding the translation to the WordPress theme language folder.

Example:

function my_theme_load_theme_textdomain() {
load_theme_textdomain( 'my-theme', FALSE, basename( dirname( __FILE__ ) ) . '/languages/' );
}
add_action( 'after_setup_theme', 'my_theme_load_theme_textdomain' );

Top ↑

Resources #