Codex

Interested in functions, hooks, classes, or methods? Check out the new WordPress Code Reference!

Difference between revisions of "I18n for WordPress Developers"

m (adding link to Urban Giraffe)
m (Descriptions: Clarify the adjacent line requirement for translator comments)
 
(159 intermediate revisions by 62 users not shown)
Line 1: Line 1:
  +
The [https://developer.wordpress.org/plugin/internationalization/how-to-internationalize-your-plugin/ plugin internationalization documentation] is now located in the [https://developer.wordpress.org/plugins/ Plugin Developer Handbook].
  +
  +
The [https://developer.wordpress.org/plugin/internationalization/localization/ plugin localization documentation] is now located in the [https://developer.wordpress.org/plugins/ Plugin Developer Handbook].
  +
  +
The [https://developer.wordpress.org/theme/functionality/internationalization/ theme internationalization documentation] is now located in the [https://developer.wordpress.org/themes/getting-started/ Theme Developer Handbook].
  +
  +
The [https://developer.wordpress.org/theme/functionality/localization/ theme localization documentation] is now located in the [https://developer.wordpress.org/themes/getting-started/ Theme Developer Handbook].
  +
 
== What is I18n? ==
 
== What is I18n? ==
I18n is a abbreviation for ''internationalization'', or the process of making an application ready for translation. In the WordPress case it means marking strings, which should be translated in a special way. It is called i18n, because there are 18 letters between the I and the n.
+
Internationalization is the process of developing your plugin so it can easily be translated into other languages. Localization describes the subsequent process of translating an internationalized plugin. Internationalization is often abbreviated as i18n (because there are 18 letters between the i and the n) and localization is abbreviated as l10n (because there are 10 letters between the l and the n.)
  +
  +
== Why is internationalization important? ==
  +
Because WordPress is used all over the world, it is a good idea to prepare a WordPress plugin so that it can be easily translated into whatever language is needed. As a developer, you may not have an easy time providing localizations for all your users; you may not speak their language after all. However, any developer can successfully internationalize a theme to allow others to create a localization without the need to modify the source code itself.
   
 
== Introduction to Gettext ==
 
== Introduction to Gettext ==
WordPress uses the gettext libraries and tools for i18n.
+
WordPress uses the [http://www.gnu.org/software/gettext/ gettext] libraries and tools for i18n. Gettext is an old and respectable piece of software, widely used in the open-source world.
   
  +
Here is how it works in a few sentences:
=== Translatable strings ===
 
In order to make a string translatable in your application you have to just wrap the original string in a <tt>__</tt> function call:
 
   
  +
* Developers wrap translatable strings in special gettext functions
<tt>$hello = __("Hello, dear user!");</tt>
 
  +
* Special tools parse the source code files and extract the translatable strings into POT (Portable Objects Template) files
  +
* In the WordPress world, POT files are often fed to GlotPress, which is a collaboration tool for translators
  +
* Translators translate the strings and the result is a PO file (POT file, but with translations inside)
  +
* PO files are compiled to binary MO files, which give faster access to the strings at run-time
   
  +
If you need to remember one thing: '''translatable strings are parsed from special function calls in the source-code, they are not obtained at run-time'''.
If your code should echo the string to the browser, use the <tt>_e</tt> function instead:
 
   
  +
Note that if you look online, you'll see the <tt>_()</tt> function which refers to the native PHP gettext-compliant translation function, but instead with WordPress you should use the [[Function Reference/_2|<tt>__()</tt>]] wordpress-defined PHP function.
<tt>_e("Your Ad here")</tt>
 
   
=== POT files ===
+
== Text Domains ==
  +
If you're translating a plugin or a theme, you'll need to use a text domain to denote all text belonging to that plugin. This increases portability and plays better with already existing WordPress tools. The text domain must match the “slug” of the plugin.
After the strings are marked in the source files, a gettext utility called <tt>xgettext</tt> is used to extract the original strings and to build a template translation <tt>POT</tt> file. Here is an example <tt>POT</tt> file entry:
 
<pre>#: wp-admin/admin-header.php:49
 
msgid "Sign Out"
 
msgstr ""</pre>
 
   
  +
The Text Domain needs to be added to the plugin header. WordPress should internationalize your plugin or theme meta-data when it displays your plugin in the admin screens:
=== PO files ===
 
Every translator takes the WordPress <tt>POT</tt> file and translates the <tt>msgstr</tt> sections in their own language. The result is a <tt>PO</tt> file with in the same format as a <tt>POT</tt>, but with translations and some specific headers.
 
   
  +
/*
=== MO files ===
 
  +
* Plugin Name: My Plugin
From a translated <tt>PO</tt> file a <tt>MO</tt> file is built. This is a binary file which contains all the original strings and their translations in a format suitable for fast translation extraction. The conversion is done using the <tt>msgfmt</tt> tool.
 
  +
* Author: Otto
  +
* Text Domain: my-plugin
  +
*/
  +
  +
The text domain is a unique identifier, which makes sure WordPress can distinguish between all loaded translations. If your plugin is a single file called <code>my-plugin.php</code> or it is contained in a folder called <code>my-plugin</code> the domain name should be <code>my-plugin</code>. The text domain name must use dashes and not underscores.
  +
  +
In general, an application may use more than one large logical translatable module and a different <code>MO</code> file accordingly. A text domain is a handle to each of these modules, which has a different <code>MO</code> file.
  +
  +
== Strings for Translation ==
  +
  +
=== Translatable strings ===
  +
In order to make a string translatable in your application you have to just wrap the original string in a <code>__()</code> function:
  +
  +
<pre>__( 'Hello, dear user!', 'my-text-domain' );</pre>
   
  +
If your code should echo the string to the browser, use the _e() function instead:
=== Text Domains ===
 
Sometimes in one application you need to use more than one large logical translatable module and a different <tt>MO</tt> file accordingly. A domain is a handle of each module, which has a different <tt>MO</tt> file. In WordPress translations of themes and plugins live in different domains.
 
   
  +
<pre>_e( 'Your Ad here', 'my-text-domain' );</pre>
If you want to get a broader and deeper view of gettext, we recommend you the [http://www.gnu.org/software/gettext/manual/html_node/ gettext online manual].
 
   
  +
The strings for translation are wrapped in a call to one of a set of special functions. The most commonly used one is <code>esc_html__()</code>. It escapes and returns the translation of its argument:
== Marking Strings for Translation ==
 
  +
<pre>echo '<h2>' . esc_html__( 'Blog Options', 'my-text-domain' ) . '</h2>';</pre>
The strings for translation are wrapped in a call to one of a set of special functions. The most commonly used one is <tt>__()</tt>. It just returns the translation of its argument:
 
  +
Another similar function is <tt>esc_html_e()</tt>, which escapes and echos the translation of its argument:
<pre>echo "<h2>".__('Blog Options')."</h2>";</pre>
 
Another simple one is <tt>_e()</tt>, which outputs the translation of its argument. Instead of writing <tt>echo __('Using this option you will make a fortune!');</tt> you can use the shorter <tt>_e('Using this option you will make a fortune!');</tt>
+
<pre>esc_html_e( 'Using this option you will make a fortune!', 'my-text-domain' );</pre>
   
 
=== Placeholders ===
 
=== Placeholders ===
<pre>echo "We deleted $count spams."</pre>
+
<pre>echo 'We deleted $count spam messages.'</pre>
 
How would you i18n this line? Let's give it a try together:
 
How would you i18n this line? Let's give it a try together:
<pre>_e("We deleted $count spams.");</pre>
+
<pre>esc_html_e( "We deleted $count spam messages.", 'my-text-domain' );</pre>
It won't work! Remember, the strings for translation are extracted from the sources, so the translators will see work on the phrase: ''We deleted $count spams.''. However in the application <tt>_e</tt> will be called with an argument like ''We deleted 49494 spams.'' and <tt>gettext</tt> won't find a suitable translation of this one and will return its argument: ''We deleted 49494 spams.''. Unfortunately, it isn't translated correctly.
+
It won't work! Remember, the strings for translation are extracted from the sources, so the translators will see work on the phrase: ''"We deleted $count spam messages."''. However in the application <code>_e</code> will be called with an argument like ''"We deleted 49494 spam messages."'' and <code>gettext</code> won't find a suitable translation of this one and will return its argument: ''"We deleted 49494 spam messages."''. Unfortunately, it isn't translated correctly.
   
The solution is to use the <tt>printf</tt> family of functions. Especially helpful are [http://php.net/printf printf] and [http://php.net/sprintf sprintf]. Here is what the right solution of the spams count problem will look like:
+
The solution is to use the <code>printf</code> family of functions. Especially helpful are [http://php.net/printf printf] and [http://php.net/sprintf sprintf]. Here is what the right solution of the spams count problem will look like:
<pre>printf(__("We deleted %d spams."), $count);</pre>
+
<pre>printf( esc_html__( 'We deleted %d spam messages.', 'my-text-domain' ), $count );</pre>
Notice that here the string for translation is just the template ''We deleted %d spams.'', which is the same both in the source and at run-time.
+
Notice that here the string for translation is just the template ''"We deleted %d spam messages."'', which is the same both in the source and at run-time.
   
If you have more than one placeholders, please allow [http://php.net/sprintf#id3677438 argument swapping].
+
If you have more than one placeholder in a string, it is recommended that you use [http://www.php.net/manual/en/function.sprintf.php#example-6070 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.
<pre>printf(__("Your city is %1$s, and your zip code is %2$s."));</pre>
+
<pre>printf( esc_html__( 'Your city is %1$s, and your zip code is %2$s.', 'my-text-domain' ), $city, $zipcode );</pre>
Here the original author of the string implied an order of the city and zip code. However in some countries it would be more suitable to list the zip first and city second. If you had used <tt>%s</tt> you wouldn't have given the translator the opportunity to swap them.
+
Here the zip code is being displayed after the city name. In some languages displaying the zip code and city in reverse order would be more appropriate. A translation can thereby be written:
  +
<pre>Your zip code is %2$s, and your city is %1$s.</pre>
  +
  +
=== HTML ===
  +
Including HTML in translatable strings depends on the context. Include HTML if the string is not separated from any text surrounding it. If the latter is unavoidable, and since translations should not be considered trusted strings, be sure to sanitize the result before echoing.
  +
  +
Example of a link (separated from text surrounding it):
  +
<pre>
  +
<div class="site-info">
  +
<a href="http://wordpress.org/" ><?php esc_html_e( 'Proudly powered by WordPress.', 'my-text-domain' ); ?></a>
  +
</div><!-- .site-info -->
  +
</pre>
  +
Example of a link in a paragraph (not separated from text surrounding it), using <code>[https://codex.wordpress.org/Function_Reference/wp_kses wp_kses()]</code> to ensure the safety of the resulting string:
  +
<pre>
  +
<p>
  +
<?php
  +
$url = 'http://example.com';
  +
$link = sprintf( wp_kses( __( 'Check out this link to my <a href="%s">website</a> made with WordPress.', 'my-text-domain' ), array( 'a' => array( 'href' => array() ) ) ), esc_url( $url ) );
  +
echo $link;
  +
?>
  +
</p>
  +
</pre>
   
 
=== Plurals ===
 
=== Plurals ===
Let's get back to the spams example: <tt>printf(__("We deleted %d spams."), $count);</tt>. What if we delete only one spam? The output will be: ''We deleted 1 spams.'', which is definitely not correct English, and probably any other language.
+
Let's get back to the spam comments example. What if we delete only one spam comment? The output will be: ''We deleted 1 spam messages.'', which is definitely not correct English, and would certainly be incorrect for many other languages as well.
   
In WordPress you can use the <tt>_n</tt> function.
+
In WordPress you can use the <code>[http://codex.wordpress.org/Function_Reference/_n _n()]</code> function.
<pre>printf(_n("We deleted %d spam.", "We deleted %d spams.", $count), $count);</pre>
+
<pre>printf( esc_html( _n( 'We deleted %d spam message.', 'We deleted %d spam messages.', $count, 'my-text-domain' ) ), $count );</pre>
<tt>_n</tt> accepts 3 arguments:
+
<code>_n()</code> accepts 4 arguments:
 
* singular &mdash; the singular form of the string
 
* singular &mdash; the singular form of the string
 
* plural &mdash; the plural form of the string
 
* plural &mdash; the plural form of the string
* count &mdash; the number of objects, which will determine if the singular or the plural form to be returned (there are languages, which have far more than 2 forms)
+
* count &mdash; 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 return value of the functions is the correct translated form, corresponding to the given count.
 
The return value of the functions is the correct translated form, corresponding to the given count.
  +
  +
Note that some languages use the singular form for other numbers (e.g. 21, 31 and so on, much like '21st', '31st' in English). If you would like to special case the singular, check for it specifically:
  +
<pre>if ( 1 === $count ) {
  +
printf( esc_html__( 'Last thing!', 'my-text-domain' ), $count );
  +
} else {
  +
printf( esc_html( _n( '%d thing.', '%d things.', $count, 'my-text-domain' ) ), $count );
  +
}</pre>
  +
  +
Also note that the <code>$count</code> parameter is often used twice. First <code>$count</code> is passed to <code>_n()</code> to determine which translated string to use, and then <code>$count</code> is passed to <code>printf()</code> to substitute the number into the translated string.
   
 
=== Disambiguation by context ===
 
=== 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 <tt>_x()</tt> function should be used. It is similar to <tt>__()</tt>, but it has an additional second argument -- the context:
+
Sometimes a single term is used in several contexts. Although it is one and the same word in English, it may need to be translated differently in some 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 <tt>[[Function_Reference/_x|_x()]]</tt> function should be used. It is similar to <tt>[[Function_Reference/_2|__()]]</tt>, but it has an additional second argument -- the context:
<pre>if ( false === $commenttxt ) $commenttxt = _x( 'Comment', 'noun' );
+
<pre>if ( false === $commenttxt ) $commenttxt = _x( 'Comment', 'noun', 'my-text-domain' );
if ( false === $trackbacktxt ) $trackbacktxt = __( 'Trackback' );
+
if ( false === $trackbacktxt ) $trackbacktxt = __( 'Trackback', 'my-text-domain' );
if ( false === $pingbacktxt ) $pingbacktxt = __( 'Pingback' );
+
if ( false === $pingbacktxt ) $pingbacktxt = __( 'Pingback', 'my-text-domain' );
 
...
 
...
 
// some other place in the code
 
// some other place in the code
echo _x('Comment', 'column name');</pre>
+
echo _x( 'Comment', 'column name', 'my-text-domain' );</pre>
  +
  +
Using this method, we will see the string "Comment" for both of the original versions, but the translators will see two "Comment" strings for translation, each in the different contexts.
  +
  +
If the translation needs escaping, use [[Function_Reference/esc_attr_x|esc_attr_x()]] or [[Function_Reference/esc_html_x|esc_html_x()]].
  +
  +
Note that similarly to <code>__()</code>, <code>_x()</code> has an 'echo' version: <code>[[Function_Reference/_ex|_ex()]]</code>. The previous example could be written as:
  +
<pre>_ex( 'Comment', 'column name', 'my-text-domain' );</pre>
  +
  +
Use whichever you feel enhances legibility and ease-of-coding.
   
  +
To add contexts to a string with plural form(s), use [[Function_Reference/_nx|_nx()]].
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.
 
   
 
=== Descriptions ===
 
=== Descriptions ===
Do you think translators will know how to translate a string like: <tt>__('g:i:s a')</tt>? In this case you can add a clarifying comment in the source code. It has to start with the words <tt>translators:</tt> and to be the last PHP comment before the gettext call. Here is an example:
+
Do you think translators will know how to translate the string below?
  +
<pre>esc_html__( 'g:i:s a', 'my-text-domain' )</pre>
  +
In this case you can add a clarifying comment in the source code. It has to start with the words <code>translators:</code> and be the last PHP comment before the gettext call (either in the same line or in the line immediately before). Here is an example:
 
<pre>/* translators: draft saved date format, see http://php.net/date */
 
<pre>/* translators: draft saved date format, see http://php.net/date */
$draft_saved_date_format = __('g:i:s a');</pre>
+
$draft_saved_date_format = esc_html__( 'g:i:s a', 'my-text-domain' );</pre>
By adding a <tt>translators:</tt> comment you can write a "personal" message to the translators, so that they know how to deal with the string.
+
By adding a <code>translators:</code> comment you can write a "personal" message to the translators, so that they know how to deal with the string.
   
 
=== Newline characters ===
 
=== Newline characters ===
   
Gettext doesn't like <tt>\r</tt> (ASCII code: 13) in translatable strings, so please avoid it and use <tt>\n</tt> instead.
+
Gettext doesn't like <code>\r</code> (ASCII code: 13) in translatable strings, so please avoid it and use <code>\n</code> instead.
   
=== Handling JavaScript files ===
+
=== 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.
== Best Practices ==
 
   
  +
If you have a valid use-case to internationalize an empty string, [[#Disambiguation_by_context|add context]] to both help translators and be in peace with the Gettext system.
Until we gather some WordPress-specific examples, use your time to read the short, but excellent article in the [http://www.gnu.org/software/gettext/manual/html_node/Preparing-Strings.html#Preparing-Strings gettext manual]. Summarized, it looks like this:
 
* Decent English style &mdash; minimize slang and abbreviations.
 
* Entire sentences &mdash; in most languages word order is different than that in English.
 
* Split at paragraphs &mdash; merge related sentences, but do not include whole page of text in one string.
 
* Use format strings instead of string concatenation &mdash; <tt>sprintf(__('Replace %s with %s'), $a, $b);</tt> is always better than <tt>__('Replace ').$a.__(' with ').$b; </tt>.
 
* Avoid unusual markup and unusual control characters &mdash; do not include tags that surround your text and do not leave URLs for translation, unless they could have version in another language.
 
   
  +
=== Handling JavaScript files ===
== I18n for theme and plugin developers ==
 
=== Choosing and loading a domain ===
 
   
  +
Use <code>wp_localize_script()</code> to add translated strings or other server-side data to a previously enqueued script.
The text domain is a unique identifier, which makes sure WordPress can distinguish between all loaded translations. Using the basename of your plugin is always a good choice.
 
   
  +
<pre>
Example: if your plugin is a single file called <tt>shareadraft.php</tt> or it is contained in a folder called <tt>shareadraft</tt> the best domain name you can choose is <tt>shareadraft</tt>. In case of a theme &mdash; choose the directory name.
 
  +
wp_enqueue_script( 'script-handle', &hellip; );
  +
wp_localize_script( 'script-handle', 'objectL10n', array(
  +
'speed' => $distance / $time,
  +
'submit' => esc_html__( 'Submit', 'my-text-domain' ),
  +
) );
  +
</pre>
   
  +
Then in the JavaScript file, corresponding to <code>script-handle</code> you can use <code>objectL10n.variable</code>:
The domain name is also used to form the name of the MO file with your plugins' translations. You can load them by invoking:
 
load_plugin_textdomain( $domain, $path_from_abspath, $path_from_plugins_folder )
 
   
  +
<pre>
as early as the <tt>init</tt> action.
 
  +
$('#submit').val(objectL10n.submit);
  +
$('#speed').val('{speed} km/h'.replace('{speed}', objectL10n.speed));
  +
</pre>
  +
  +
=== I18n for widgets ===
  +
[[Version 2.8|WordPress 2.8+]] uses a new [[Widget API]], that only requires the widget developer to extend the standard widget class and some of its functions. With this API there is no <tt>init</tt> function. After the widget is coded using the <code>widget()</code>, <code>form()</code>, and <code>update()</code> methods, the widget must be registered. The textdomain is then loaded after the widget is registered.
   
 
Example:
 
Example:
   
  +
// register Foo_Widget widget
$plugin_dir = basename(dirname(__FILE__));
 
  +
function Foo_Widget_init() {
load_plugin_textdomain( 'myplugin', 'wp-content/plugins/' . $plugin_dir, $plugin_dir );
 
  +
return register_widget( 'Foo_Widget' );
  +
}
  +
add_action( 'widgets_init', 'Foo_Widget_init' );
  +
  +
$plugin_dir = basename( dirname( __FILE__ ) );
  +
load_plugin_textdomain( 'foo_widget', null, $plugin_dir );
   
  +
This example registers a widget named ''Foo_Widget'', then sets the plugin directory variable and attempts to load the <tt>foo_widget-''locale''.po</tt> file.
This call tries to load <tt>myplugin.''locale''.mo</tt> from your plugin's base directory. The locale consistes of a language code and country code separated by an underscore. (For more information about language and country codes, see [[Installing_WordPress_in_Your_Language|Installing WordPress in Your Language]].)
 
   
  +
=== Best Practices ===
The second and third parameters are present because of a change that occurred in WordPress 2.6. For versions lower than 2.6, the second parameter should be the directory containing the .mo file, relative to ABSPATH.
 
   
  +
Until we gather some WordPress-specific examples, use your time to read the short, but excellent article in the [http://www.gnu.org/software/gettext/manual/html_node/Preparing-Strings.html#Preparing-Strings gettext manual]. Summarized, it looks like this:
For WordPress 2.6 and up, the third parameter is the directory containing the .mo file, relative to the plugins directory. (Thus, if you plugin doesn't need compatibility with older versions of WordPress, you can leave the second parameter blank.)
 
  +
* Decent English style&mdash;minimize slang and abbreviations.
  +
* Entire sentences&mdash;in most languages word order is different than that in English.
  +
* Split at paragraphs&mdash;merge related sentences, but do not include a whole page of text in one string.
  +
* Use format strings instead of string concatenation&mdash;<tt>sprintf(__('Replace %1$s with %2$s'), $a, $b);</tt> is always better than <tt>__('Replace ').$a.__(' with ').$b; </tt>.
  +
* Avoid unusual markup and unusual control characters&mdash;do not include tags that surround your text and do not leave URLs for translation, unless they could have a version in another language.
  +
* Do not leave leading or trailing whitespace in a translatable phrase.
   
  +
=== Loading a Text Domain ===
For themes the process is surprisingly similar:
 
load_theme_textdomain(''domain-name'');
 
Put this call in your <tt>functions.php</tt> and it will search your theme directory for <tt>''locale''.mo</tt> and load it (where ''locale'' is the current language, i.e. ''pt_BR.mo'').
 
   
  +
The text domain name is also used to form the name of the MO file for your plugin. You can load the file by calling the function [[Function Reference/load_plugin_textdomain|load_plugin_textdomain]] as early as the <tt>plugins_loaded</tt> [[Plugin_API#Actions|action]].
==== I18n for widgets developed on 2.8+ ====
 
  +
load_plugin_textdomain( $domain, $path_from_abspath, $path_from_plugins_folder )
WordPress 2.8+ uses a new widget API, that only requires the widget developer to extend the standard widget class and some of it's functions. With this new API there is no <tt>init</tt> function. After the widget is coded using the widget(), form(), and update() functions, the widget must be registered. The text-domain is then loaded after the widget is registered.
 
   
 
Example:
 
Example:
// register FooWidget widget
 
add_action('widgets_init', create_function('', 'return register_widget("FooWidget");'));
 
$plugin_dir = basename(dirname(__FILE__));
 
load_plugin_textdomain( 'FooWidget', 'wp-content/plugins/' . $plugin_dir, $plugin_dir );
 
This example registers a widget named ''FooWidget'', then sets the plugin directory variable and attempts to load the <tt>FooWidget-''locale''.po</tt> file.
 
   
  +
<pre>function myplugin_init() {
=== Marking strings in themes and plugins ===
 
  +
$plugin_rel_path = basename( dirname( __FILE__ ) ) . '/languages'; /* Relative to WP_PLUGIN_DIR */
  +
load_plugin_textdomain( 'my-plugin', false, $plugin_rel_path );
  +
}
  +
add_action('plugins_loaded', 'myplugin_init');</pre>
  +
  +
This call tries to load <tt>my-plugin-''{locale}''.mo</tt> from your plugin directory. The ''locale'' is the language code and/or country code you defined in the constant <tt>WPLANG</tt> in the file <tt>wp-config.php</tt>.
  +
  +
For example, the locale for German is 'de', and the locale for Danish is 'da_DK'. The MO files for 'my-plugin' should be named <tt>my-plugin-de.mo</tt> and <tt>my-plugin-da_DK.po</tt>. For more information about language and country codes, see [[Installing_WordPress_in_Your_Language|Installing WordPress in Your Language]].
  +
  +
* For ''WordPress 2.6 and up'', the ''third parameter'' is the directory containing the .mo file, ''relative to the plugin directory''. <b>It must end with a trailing slash.</b> If your plugin doesn't need compatibility with older versions of WordPress, you can leave the second parameter blank.
  +
  +
* For ''versions lower than 2.6'', the ''second parameter'' should be the directory containing the .mo file, ''relative to ABSPATH''. The third parameter should be blank.
  +
  +
For themes the process is surprisingly similar:
  +
load_theme_textdomain('my_theme');
  +
Put this call in your <tt>functions.php</tt> and it will search your theme directory for <tt>''locale''.mo</tt> and load it (where ''locale'' is the current language, i.e. ''he_IL.mo'').
   
  +
<b>Watch Out</b>
All the rules from [[#Marking_Strings_for_Translation|above]] apply here, but there is one more. The additional rule states that '''you must add your domain as an argument to every __, _e, _c and __ngettext call''', otherwise '''your translations won't work'''.
 
  +
* '''DO''' name your MO file as <tt>''locale''.mo</tt> (e.g., <tt>he_IL.mo</tt>)
  +
* '''DO NOT''' name your MO file as <tt>my_theme-he_IL.mo</tt>
   
  +
=== Marking strings with Text Domain ===
Examples:
 
   
  +
'''You must add your domain as an argument to every __, _e and _n gettext call''', otherwise '''your translations won't work'''.
* <tt>__('String')</tt> should become <tt>__('String', 'domain')</tt>
 
* <tt>_e('String')</tt> should become <tt>_e('String', 'domain')</tt>
 
* <tt>__ngettext('String', 'Strings', $c)</tt> should become <tt>__ngettext('String', 'Strings', $c, 'domain')</tt>
 
   
 
Adding the domain by hand is a burden and that's why you can do it automatically:
 
Adding the domain by hand is a burden and that's why you can do it automatically:
   
* If your plugin is registered in the [http://wordpress.org/extend/plugins/ official repository], go to your '''Admin''' page there and scroll to '''Add Domain to Gettext Calls'''.
+
If your plugin is registered in the [http://wordpress.org/extend/plugins/ official repository]:
  +
* Go to your '''Admin''' page there and scroll to '''Add Domain to Gettext Calls'''.
  +
 
Otherwise:
 
Otherwise:
 
* Get the [http://svn.automattic.com/wordpress-i18n/tools/trunk/add-textdomain.php add-textdomain.php] script and execute it like this:
 
* Get the [http://svn.automattic.com/wordpress-i18n/tools/trunk/add-textdomain.php add-textdomain.php] script and execute it like this:
Line 149: Line 243:
 
After it's done, the domain will be added to all gettext calls in the files.
 
After it's done, the domain will be added to all gettext calls in the files.
   
=== Generating a POT file ===
+
== Translating Plugins and Themes ==
   
  +
=== POT files ===
You remember the [[#POT_Files|POT file]] is the one you need to hand to translators, so that they can do their work, don't you?
 
  +
The first stage is to generate a <tt>.pot</tt> for your plugin or theme. This is done by way of the <tt>xgettext</tt> utility as part of gettext. You will need to have the gettext package installed if you want to do this generation on-site.
   
  +
==== Using WP-CLI ====
Once that is in place, there are a couple of ways to generate a POT file for your plugin:
 
# If your plugin is registered in the [http://wordpress.org/extend/plugins/ official repository], go to your '''Admin''' page there and scroll to '''Generate POT file'''.
 
# If your plugin is not in the repository, you can checkout the [http://svn.automattic.com/wordpress-i18n/tools/trunk/ wordpress-i18n tools] directory from SVN (see [[Using Subversion]] to learn about SVN) and then run the <tt>makepot.php</tt> script like this:
 
php makepot.php wp-plugin ''your-plugin-directory''
 
   
  +
Install [https://make.wordpress.org/cli/handbook/installing/ WP-CLI] and use the <tt>wp i18n make-pot</tt> command according to the [https://developer.wordpress.org/cli/commands/i18n/make-pot/ documentation].
You need the gettext (GNU Internationalization utilities) package to be installed in your server before you can run the above command
 
  +
After it's finished you should see the POT file in the current directory.
 
  +
==== Using Grunt ====
  +
  +
If you use [http://gruntjs.com Grunt] with your theme or plugin, you can use the [https://github.com/stephenharris/grunt-pot grunt-pot] plugin by Stephen Harris to generate a <tt>.pot</tt> file. See [http://stephenharris.info/grunt-wordpress-development-iii-tasks-for-internationalisation/ his site] for instructions on integrating it into your project.
  +
  +
==== Example content ====
  +
  +
Each translatable string is formatted like this:
  +
  +
<pre>#: comments.php:28
  +
msgid "Comments:"
  +
msgstr ""</pre>
  +
  +
=== PO files ===
  +
Every translator takes the WordPress <tt>.pot</tt> file and translates the <tt>msgstr</tt> sections to their own language. The result is a <tt>.po</tt> file with the same format as a <tt>.pot</tt>, but with translations and some specific headers.
  +
  +
=== MO files ===
  +
From a resulting <tt>.po</tt> translation file a <tt>.mo</tt> file is compiled. This is a binary file which contains all the original strings and their translations in a format suitable for fast translation extraction. The conversion is done using the <tt>msgfmt</tt> tool:
   
  +
msgfmt -o &lt;output&gt;.mo &lt;input&gt;.po
It is a good idea to offer the POT file along with your plugin, so that translators won't have to ask you specifically about it.
 
   
  +
If you have a lot of <tt>.po</tt> files to convert at once, you can run it as a batch. For example, using a <tt>bash</tt> command:
Also, if you add a line like this to your plugin header, WordPress should internationalize your plugin meta-data when it displays your plugin in the admin screens:
 
<code>
+
<pre>
  +
# Find PO files, process each with msgfmt and rename the result to MO
Text Domain: your-text-domain
 
  +
for file in `find . -name "*.po"` ; do msgfmt -o ${file/.po/.mo} $file ; done
</code>
 
  +
</pre>
   
 
==Resources==
 
==Resources==
  +
* [https://pascalbirchler.com/text-domain-wordpress-internationalization/ The text domain in WordPress internationalization]
[http://urbangiraffe.com/articles/localizing-wordpress-themes-and-plugins/ Localizing WordPress Themes and Plugins]
 
  +
* [http://ottopress.com/2013/language-packs-101-prepwork/ Language Packs 101 – Prepwork]
  +
* [http://clivern.com/how-to-internationalize-your-wordpress-plugin/ How to internationalize your wordpress plugin]
  +
* [https://wpgeodirectory.com/loading-wordpress-language-files-correctly/ Loading WordPress language files correctly]
  +
* [http://markjaquith.wordpress.com/2011/10/06/translating-wordpress-plugins-and-themes-dont-get-clever/ Translating WordPress Plugins and Themes: Don’t Get Clever]
  +
* [http://wp.smashingmagazine.com/2011/12/29/internationalizing-localizing-wordpress-theme/ Internationalizing And Localizing Your WordPress Theme]
  +
* [http://ottopress.com/2012/internationalization-youre-probably-doing-it-wrong/ Internationalization: You’re probably doing it wrong]
  +
* [http://ottopress.com/2012/more-internationalization-fun/ More Internationalization Fun]
  +
* [https://github.com/wpapps/wp-pot-generator Github WP Pot Generator Action]
   
 
[[Category:Advanced Topics]]
 
[[Category:Advanced Topics]]

Latest revision as of 11:22, 23 July 2020

The plugin internationalization documentation is now located in the Plugin Developer Handbook.

The plugin localization documentation is now located in the Plugin Developer Handbook.

The theme internationalization documentation is now located in the Theme Developer Handbook.

The theme localization documentation is now located in the Theme Developer Handbook.

What is I18n?

Internationalization is the process of developing your plugin so it can easily be translated into other languages. Localization describes the subsequent process of translating an internationalized plugin. Internationalization is often abbreviated as i18n (because there are 18 letters between the i and the n) and localization is abbreviated as l10n (because there are 10 letters between the l and the n.)

Why is internationalization important?

Because WordPress is used all over the world, it is a good idea to prepare a WordPress plugin so that it can be easily translated into whatever language is needed. As a developer, you may not have an easy time providing localizations for all your users; you may not speak their language after all. However, any developer can successfully internationalize a theme to allow others to create a localization without the need to modify the source code itself.

Introduction to Gettext

WordPress uses the gettext libraries and tools for i18n. Gettext is an old and respectable piece of software, widely used in the open-source world.

Here is how it works in a few sentences:

  • Developers wrap translatable strings in special gettext functions
  • Special tools parse the source code files and extract the translatable strings into POT (Portable Objects Template) files
  • In the WordPress world, POT files are often fed to GlotPress, which is a collaboration tool for translators
  • Translators translate the strings and the result is a PO file (POT file, but with translations inside)
  • PO files are compiled to binary MO files, which give faster access to the strings at run-time

If you need to remember one thing: translatable strings are parsed from special function calls in the source-code, they are not obtained at run-time.

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.

Text Domains

If you're translating a plugin or a theme, you'll need to use a text domain to denote all text belonging to that plugin. This increases portability and plays better with already existing WordPress tools. The text domain must match the “slug” of the plugin.

The Text Domain needs to be added to the plugin header. WordPress should internationalize your plugin or theme meta-data when it displays your plugin in the admin screens:

/*
 * Plugin Name: My Plugin
 * Author: Otto
 * Text Domain: my-plugin
 */

The text domain is a unique identifier, which makes sure WordPress can distinguish between all loaded translations. If your plugin is a single file called my-plugin.php or it is contained in a folder called my-plugin the domain name should be my-plugin. The text domain name must use dashes and not underscores.

In general, an application may use more than one large logical translatable module and a different MO file accordingly. A text domain is a handle to each of these modules, which has a different MO file.

Strings for Translation

Translatable strings

In order to make a string translatable in your application you have to just wrap the original string in a __() function:

__( 'Hello, dear user!', 'my-text-domain' );

If your code should echo the string to the browser, use the _e() function instead:

_e( 'Your Ad here', 'my-text-domain' );

The strings for translation are wrapped in a call to one of a set of special functions. The most commonly used one is esc_html__(). It escapes and returns the translation of its argument:

echo '<h2>' . esc_html__( 'Blog Options', 'my-text-domain' ) . '</h2>';

Another similar function is esc_html_e(), which escapes and echos the translation of its argument:

esc_html_e( 'Using this option you will make a fortune!', 'my-text-domain' );

Placeholders

echo 'We deleted $count spam messages.'

How would you i18n this line? Let's give it a try together:

esc_html_e( "We deleted $count spam messages.", 'my-text-domain' );

It won't work! Remember, the strings for translation are extracted from the sources, so the translators will see work on the phrase: "We deleted $count spam messages.". However in the application _e will be called with an argument like "We deleted 49494 spam messages." and gettext won't find a suitable translation of this one and will return its argument: "We deleted 49494 spam messages.". Unfortunately, it isn't translated correctly.

The solution is to use the printf family of functions. Especially helpful are printf and sprintf. Here is what the right solution of the spams count problem will look like:

printf( esc_html__( 'We deleted %d spam messages.', 'my-text-domain' ), $count );

Notice that here the string for translation is just the template "We deleted %d spam messages.", 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( esc_html__( 'Your city is %1$s, and your zip code is %2$s.', 'my-text-domain' ), $city, $zipcode );

Here the zip code is being displayed after the city name. In some languages displaying the zip code and city in reverse order would be more appropriate. A translation can thereby be written:

Your zip code is %2$s, and your city is %1$s.

HTML

Including HTML in translatable strings depends on the context. Include HTML if the string is not separated from any text surrounding it. If the latter is unavoidable, and since translations should not be considered trusted strings, be sure to sanitize the result before echoing.

Example of a link (separated from text surrounding it):

<div class="site-info">
  <a href="http://wordpress.org/" ><?php esc_html_e( 'Proudly powered by WordPress.', 'my-text-domain' ); ?></a>
</div><!-- .site-info -->

Example of a link in a paragraph (not separated from text surrounding it), using wp_kses() to ensure the safety of the resulting string:

<p>
<?php
$url = 'http://example.com';
$link = sprintf( wp_kses( __( 'Check out this link to my <a href="%s">website</a> made with WordPress.', 'my-text-domain' ), array(  'a' => array( 'href' => array() ) ) ), esc_url( $url ) );
echo $link;
?>
</p>

Plurals

Let's get back to the spam comments example. What if we delete only one spam comment? The output will be: We deleted 1 spam messages., which is definitely not correct English, and would certainly be incorrect for many other languages as well.

In WordPress you can use the _n() function.

printf( esc_html( _n( 'We deleted %d spam message.', 'We deleted %d spam messages.', $count, 'my-text-domain'  ) ), $count );

_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 return value of the functions is the correct translated form, corresponding to the given count.

Note that some languages use the singular form for other numbers (e.g. 21, 31 and so on, much like '21st', '31st' in English). If you would like to special case the singular, check for it specifically:

if ( 1 === $count ) {
	printf( esc_html__( 'Last thing!', 'my-text-domain' ), $count );
} else {
	printf( esc_html( _n( '%d thing.', '%d things.', $count, 'my-text-domain' ) ), $count );
}

Also note that the $count parameter is often used twice. First $count is passed to _n() to determine which translated string to use, and then $count is passed to printf() to substitute the number into the translated string.

Disambiguation by context

Sometimes a single term is used in several contexts. Although it is one and the same word in English, it may need to be translated differently in some 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() function should be used. It is similar to __(), but it has an additional second argument -- the context:

if ( false === $commenttxt ) $commenttxt = _x( 'Comment', 'noun', 'my-text-domain' );
if ( false === $trackbacktxt ) $trackbacktxt = __( 'Trackback', 'my-text-domain' );
if ( false === $pingbacktxt ) $pingbacktxt = __( 'Pingback', 'my-text-domain' );
...
// some other place in the code
echo _x( 'Comment', 'column name', 'my-text-domain' );

Using this method, we will see the string "Comment" for both of the original versions, but the translators will see two "Comment" strings for translation, each in the different contexts.

If the translation needs escaping, use esc_attr_x() or esc_html_x().

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

_ex( 'Comment', 'column name', 'my-text-domain' );

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

To add contexts to a string with plural form(s), use _nx().

Descriptions

Do you think translators will know how to translate the string below?

esc_html__( 'g:i:s a', 'my-text-domain' )

In this case you can add a clarifying comment in the source code. It has to start with the words translators: and be the last PHP comment before the gettext call (either in the same line or in the line immediately before). Here is an example:

/* translators: draft saved date format, see http://php.net/date */
$draft_saved_date_format = esc_html__( 'g:i:s a', 'my-text-domain' );

By adding a translators: comment you can write a "personal" message to the translators, so that they know how to deal with the string.

Newline characters

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

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.

Handling JavaScript files

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

wp_enqueue_script( 'script-handle', … );
wp_localize_script( 'script-handle', 'objectL10n', array(
	'speed'  => $distance / $time,
	'submit' => esc_html__( 'Submit', 'my-text-domain' ),
) );

Then in the JavaScript file, corresponding to script-handle you can use objectL10n.variable:

$('#submit').val(objectL10n.submit);
$('#speed').val('{speed} km/h'.replace('{speed}', objectL10n.speed));

I18n for widgets

WordPress 2.8+ uses a new Widget API, that only requires the widget developer to extend the standard widget class and some of its functions. With this API there is no init function. After the widget is coded using the widget(), form(), and update() methods, the widget must be registered. The textdomain is then loaded after the widget is registered.

Example:

// register Foo_Widget widget
function Foo_Widget_init() {
    return register_widget( 'Foo_Widget' );
}
add_action( 'widgets_init', 'Foo_Widget_init' );

$plugin_dir = basename( dirname( __FILE__ ) );
load_plugin_textdomain( 'foo_widget', null, $plugin_dir );

This example registers a widget named Foo_Widget, then sets the plugin directory variable and attempts to load the foo_widget-locale.po file.

Best Practices

Until we gather some WordPress-specific examples, use your time to read the short, but excellent article in the gettext manual. Summarized, it looks like this:

  • Decent English style—minimize slang and abbreviations.
  • 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.
  • Use format strings instead of string concatenation—sprintf(__('Replace %1$s with %2$s'), $a, $b); is always better than __('Replace ').$a.__(' with ').$b; .
  • Avoid unusual markup and unusual control characters—do not include tags that surround your text and do not leave URLs for translation, unless they could have a version in another language.
  • Do not leave leading or trailing whitespace in a translatable phrase.

Loading a Text Domain

The text domain name is also used to form the name of the MO file for your plugin. You can load the file by calling the function load_plugin_textdomain as early as the plugins_loaded action.

load_plugin_textdomain( $domain, $path_from_abspath, $path_from_plugins_folder )

Example:

function myplugin_init() {
    $plugin_rel_path = basename( dirname( __FILE__ ) ) . '/languages'; /* Relative to WP_PLUGIN_DIR */
    load_plugin_textdomain( 'my-plugin', false, $plugin_rel_path );
}
add_action('plugins_loaded', 'myplugin_init');

This call tries to load my-plugin-{locale}.mo from your plugin directory. 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', and the locale for Danish is 'da_DK'. The MO files for 'my-plugin' should be named my-plugin-de.mo and my-plugin-da_DK.po. For more information about language and country codes, see Installing WordPress in Your Language.

  • For WordPress 2.6 and up, the third parameter is the directory containing the .mo file, relative to the plugin directory. It must end with a trailing slash. If your plugin doesn't need compatibility with older versions of WordPress, you can leave the second parameter blank.
  • For versions lower than 2.6, the second parameter should be the directory containing the .mo file, relative to ABSPATH. The third parameter should be blank.

For themes the process is surprisingly similar:

load_theme_textdomain('my_theme');

Put this call in your functions.php and it will search your theme directory for locale.mo and load it (where locale is the current language, i.e. he_IL.mo).

Watch Out

  • DO name your MO file as locale.mo (e.g., he_IL.mo)
  • DO NOT name your MO file as my_theme-he_IL.mo

Marking strings with Text Domain

You must add your domain as an argument to every __, _e and _n gettext call, otherwise your translations won't work.

Adding the domain by hand is a burden and that's why you can do it automatically:

If your plugin is registered in the official repository:

  • Go to your Admin page there and scroll to Add Domain to Gettext Calls.

Otherwise:

php add-textdomain.php -i domain phpfile phpfile ...

After it's done, the domain will be added to all gettext calls in the files.

Translating Plugins and Themes

POT files

The first stage is to generate a .pot for your plugin or theme. This is done by way of the xgettext utility as part of gettext. You will need to have the gettext package installed if you want to do this generation on-site.

Using WP-CLI

Install WP-CLI and use the wp i18n make-pot command according to the documentation.

Using Grunt

If you use Grunt with your theme or plugin, you can use the grunt-pot plugin by Stephen Harris to generate a .pot file. See his site for instructions on integrating it into your project.

Example content

Each translatable string is formatted like this:

#: comments.php:28
msgid "Comments:"
msgstr ""

PO files

Every translator takes the WordPress .pot file and translates the msgstr sections to their own language. The result is a .po file with the same format as a .pot, but with translations and some specific headers.

MO files

From a resulting .po translation file a .mo file is compiled. This is a binary file which contains all the original strings and their translations in a format suitable for fast translation extraction. The conversion is done using the msgfmt tool:

msgfmt -o <output>.mo <input>.po

If you have a lot of .po files to convert at once, you can run it as a batch. For example, using a bash command:

# Find PO files, process each with msgfmt and rename the result to MO
for file in `find . -name "*.po"` ; do msgfmt -o ${file/.po/.mo} $file ; done

Resources