I18n for WordPress Developers
- 1 国際化とは
- 2 Gettext 入門
- 3 Marking Strings for Translation
- 4 Best Practices
- 5 I18n for theme and plugin developers
- 6 Resources
I18n は、internationalization、すなわちアプリケーションを翻訳可能にするプロセスのことをいいます。WordPress では、決まった方法で翻訳されるべき文字列をマークすることを意味します。I と n の間に 18 文字あるため、これを i18n と呼びます。
WordPress では、gettext ライブラリおよびツールを使用して国際化します。
アプリケーションで文字列を翻訳可能にするには、元の文字列を __ 関数コールで囲います。
$hello = __("Hello, dear user!");
_e("Your Ad here")
ソースファイルで文字列をマークしたら、xgettext という gettext ユーティリティを使用して文字列を抽出し、テンプレート翻訳 POT ファイルを構築します。以下は、POT ファイルのエントリの例です。
#: wp-admin/admin-header.php:49 msgid "Sign Out" msgstr ""
翻訳者は WordPress POT ファイルを受け取り、msgstr セクションを自分の言語に翻訳します。出力は PO ファイルです。POT ファイルとフォーマットは同じですが、翻訳されていて、特定のヘッダが異なります。
翻訳済み PO ファイルから MO ファイルを構築します。このファイルは、すべての元の文字列と翻訳を含み、高速に翻訳を抽出するのに適したバイナリファイルです。msgfmt ツールを使用して変換します。
1 つのアプリケーションに、複数の巨大な論理翻訳モジュールを使用し、それらに異なる MO ファイルを使用する必要があるかもしれません。ドメインは、各モジュールをハンドルし、それぞれ異なる MO ファイルを持ちます。テーマの翻訳とプラグインの翻訳は、異なるドメインになります。
gettext に関して詳しく知りたい方は、gettext online manual を参照することをおすすめします。
Marking Strings for Translation
The strings for translation are wrapped in a call to one of a set of special functions. The most commonly used one is __(). It just returns the translation of its argument:
echo "<h2>".__('Blog Options')."</h2>";
Another simple one is _e(), which outputs the translation of its argument. Instead of writing echo __('Using this option you will make a fortune!'); you can use the shorter _e('Using this option you will make a fortune!');
echo "We deleted $count spams."
How would you i18n this line? Let's give it a try together:
_e("We deleted $count spams.");
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 _e will be called with an argument like We deleted 49494 spams. and gettext won't find a suitable translation of this one and will return its argument: We deleted 49494 spams.. Unfortunately, it isn't translated correctly.
printf(__("We deleted %d spams."), $count);
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.
If you have more than one placeholders, please allow argument swapping.
printf(__("Your city is %1$s, and your zip code is %2$s."));
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 %s you wouldn't have given the translator the opportunity to swap them.
Let's get back to the spams example: printf(__("We deleted %d spams."), $count);. 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.
In WordPress you can use the _n function.
printf(_n("We deleted %d spam.", "We deleted %d spams.", $count), $count);
_n accepts 3 arguments:
- singular — the singular form of the string
- plural — the plural form of the string
- count — 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)
The return value of the functions is the correct translated form, corresponding to the given count.
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() function should be used. It is similar to __(), but it has an additional second argument -- the context:
if ( false === $commenttxt ) $commenttxt = _x( 'Comment', 'noun' ); if ( false === $trackbacktxt ) $trackbacktxt = __( 'Trackback' ); if ( false === $pingbacktxt ) $pingbacktxt = __( 'Pingback' ); ... // some other place in the code echo _x('Comment', 'column name');
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.
Do you think translators will know how to translate a string like: __('g:i:s a')? In this case 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 */ $draft_saved_date_format = __('g:i:s a');
By adding a translators: comment you can write a "personal" message to the translators, so that they know how to deal with the string.
Gettext doesn't like \r (ASCII code: 13) in translatable strings, so please avoid it and use \n instead.
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 whole page of text in one string.
- Use format strings instead of string concatenation — sprintf(__('Replace %s with %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 version in another language.
I18n for theme and plugin developers
Choosing and loading a domain
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.
Example: if your plugin is a single file called shareadraft.php or it is contained in a folder called shareadraft the best domain name you can choose is shareadraft. In case of a theme — choose the directory name.
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 )
as early as the init action.
$plugin_dir = basename(dirname(__FILE__)); load_plugin_textdomain( 'myplugin', 'wp-content/plugins/' . $plugin_dir, $plugin_dir );
This call tries to load myplugin.locale.mo 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.)
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.
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.)
For themes the process is surprisingly similar:
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. pt_BR.mo).
I18n for widgets developed on 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 it's functions. With this new API there is no init 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.
// 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 FooWidget-locale.po file.
Marking strings in themes and plugins
All the rules from 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.
- __('String') should become __('String', 'domain')
- _e('String') should become _e('String', 'domain')
- __ngettext('String', 'Strings', $c) should become __ngettext('String', 'Strings', $c, 'domain')
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.
- Get the add-textdomain.php script and execute it like this:
php add-textdomain.php -i domain phpfile phpfile ...
After it's done, the domain will be added to all gettext calls in the files.
Generating a POT file
You remember the POT file is the one you need to hand to translators, so that they can do their work, don't you?
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 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 wordpress-i18n tools directory from SVN (see Using Subversion to learn about SVN) and then run the makepot.php script like this:
php makepot.php wp-plugin your-plugin-directory
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.
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.
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:
Text Domain: your-text-domain