Posts tagged ‘internationalization’

Olanguagesne of the new features alongside the auto-update feature in WordPress 3.7 is support for “language packs”. More info about these will be coming out eventually, along with new tools for plugin and theme authors to use to manage this system (or to not have to micro-manage it, rather). A lot of this feature is yet to be implemented on WordPress.org, but the core support for it is in WordPress 3.7.

In order to use it most effectively, there’s a few ground rules that you, as a plugin or theme author, need to follow. Fortunately, they’re pretty simple.

Text-domains = the plugin/theme slug

Firstly, for language packs to work, your text-domain must be identical to the plugin or theme’s slug.

What’s a “slug”? Good question. If you examine the URL of your plugin or theme on WordPress.org, you’ll find that it looks like this:

http://wordpress.org/plugins/some-text-here

or

http://wordpress.org/themes/some-text-here

That “some-text-here” part is the slug. It cannot be changed by the plugin or theme author once the entry is created for it in the WordPress.org directory. It is a unique item to plugins/themes, and that’s how WordPress.org will be managing and naming the language files.

Therefore, your “text-domain” must be the same as that slug. In all your translation function calls, the text-domain must be there, it must be a plain string, and it must be identical to the slug of your plugin or theme on WordPress.org.

Headers

For translation to be most effective for your plugin/theme, you need to include a header in it that you may not be including:

Text Domain: put-the-slug-here

This “Text Domain” header is read and used to load your language pack files even when your plugin is not activated. This allows the headers of the plugin (like the description and such) to be translated properly when the plugin is displayed on the Plugins/Themes screen. So your international users will be able to read that text too, before ever using the code.

If you want to include your own translation files instead of using the language pack system, then this still works. The core code will look for the relevant *.mo translation files in the plugin’s directory. If you use a subdirectory, like “/languages”, then you can use a header like the following:

Domain Path: /languages

Note that the Domain Path for plugins defaults to the plugin’s own root directory, but the Domain Path for themes defaults to “/languages” to begin with. If the default works for you, then you do not need to have this header at all.

Also note that if a language file is not found for a particular configuration, then WordPress 3.7 will fall back to using the language pack system to attempt to find it. So if you only include, say, 3 languages, and there are language packs for 4 more, then those 4 more will still work.

Speaking of configuration,

Function calls: load_plugin_textdomain or load_theme_textdomain

Here is how to properly call them, with the Headers you’ll need included for good measure:

If you want to allow for translation MO files in the plugin’s own directory:

Text Domain: plugin-slug
load_plugin_textdomain( 'plugin-slug', false, dirname( plugin_basename( __FILE__ ) ) );

If you want to allow for translation MO files in the plugin’s languages subdirectory:

Text Domain: plugin-slug
Domain Path: /languages
load_plugin_textdomain( 'plugin-slug', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );

If you want to use language packs exclusively (note: WP will still check the base /wp-content/plugins directory for language files, just in case):

Text Domain: plugin-slug
load_plugin_textdomain( 'plugin-slug' );

If you want to allow for translation MO files in the theme’s languages subdirectory:

Text Domain: theme-slug
load_theme_textdomain( 'theme-slug', get_template_directory() . '/languages' );

If you want to allow for translation MO files in the theme’s “lang” directory:

Text Domain: theme-slug
Domain Path: /lang
load_theme_textdomain( 'theme-slug', get_template_directory() . '/lang' );

If you want to use language packs exclusively (note: WP will still check the theme’s own directory for language files, just in case):

Text Domain: theme-slug
load_theme_textdomain( 'theme-slug' );

Important:

  • Any calls to load_plugin_textdomain should be in a function attached to the “plugins_loaded” action hook.
  • Any calls to load_theme_textdomain should be in a function attached to the “after_setup_theme” action hook.

How it will work

Eventually, WordPress.org will have a way to allow plugin/theme authors to upload translation files. Or, it will have a way to allow users to submit their translations to them via translate.wordpress.org… Regardless, the relevant MO files will be made on some basis, and the files will be made available to WordPress users through the normal plugin/theme update process. The auto-update system will automatically download these MO files into the /wp-content/languages directory. There will be plugins and themes subdirectories under that to hold these files.

The files will be named “slug-locale.mo”, where slug is the plugin or theme’s slug, and the locale is the relevant locale information about the language (like “en_US” for example). When load_plugin/theme_textdomain is called, WordPress will look in the specified place for the relevant MO file, and if it does not find it, then it falls back to looking in the /wp-content/languages folder for it, on that named basis. If it finds it, it loads it up and uses it.

This gives the plugin or theme authors the ability to continue to manage their translations themselves, as they’ve always done, or use the new language pack system and let WordPress.org manage it for you. The language pack system has a number of advantages:

  • Users only download the languages they actually need, instead of all of them. Your plugin is smaller, the download is faster.
  • New translations can be approved and pushed as updates independently of the plugin or theme. No more need to bump the version just to get new translations to users.
  • Translations can be handled much easier, or ignored by the author entirely. Communities can (eventually) do their own translations through translate.wordpress.org.

Things like that. These all rely on plugins and themes doing translations a certain and specific way, along with properly internationalizing their code for translation.

Obviously, any code not doing this sort of thing won’t get these benefits. Well, we can’t fix everything at once. But hopefully, the most common and popular ones will do this (or already are), and they can be integrated into the system quickly and easily.

Some tools to help

If you’re a plugin or theme author, do yourself a favor and use your SVN client to get a copy of this repository:

http://develop.svn.wordpress.org/trunk/

This is the core develop repository for WordPress. It comes with the WordPress trunk code (in /src) but it also has some important tools you’ll need in the /tools/i18n directory. Note that to use these tools, you need the *entire* checkout, not just the tools. The tools make calls back into the WordPress core code to do some of the work, so the whole /trunk directory needs to be available there.

Also, those tools are managed by the core team. So keep them to date by doing an svn update every once in a while too.

Here’s one of those tools: makepot.php

And here’s how you run it:

> php makepot.php wp-plugin /path/to/my/plugin-dir plugin-slug.pot

This will scan your plugin’s directory and create a POT file for you to give to translators or include with your plugin. Theme authors, same deal, just replace “wp-plugin” with “wp-theme”.

Here’s another tool: add-textdomain.php

It will read in a file and add a proper text-domain to all translation function calls it finds. To use it, you can do this:

> php add-textdomain.php plugin-slug /path/to/a/file.php > newfile.php

The newfile.php will be identical, but all the translation calls will be fixed up and have the plugin-slug in there as intended.

The tool outputs the new file on standard output, which I redirected into “newfile.php” as you can see above. This is so that it is non-destructive by default. If you’re confident, and have backups of the files just in case, you can use it in-place like so:

> php add-textdomain -i plugin-slug /path/to/a/file.php

The original file will be replaced with the modified version. Use this at your own risk. I’m paranoid, I prefer to make a new file for manual comparison. 😉

This tool will go through and add the text-domain to any calls where you might have left it off. I have done this many times. Force of habit, or I just forget to do it, etc.

More Info

And if you’re having a hard time with making your text translatable in the code, I have a couple other posts on that topic as well. See them too.

So go forth, plugin and theme authors. Start fixing up that code. Many of you may have nothing to fix. Some of you may just need a header change. But it’s worth giving it a once over anyway. It certainly would be very nice if, as the new features begin to be added to WordPress.org, then your code was all ready and set to take immediate advantage of it, wouldn’t it? 🙂

Shortlink:


So in my last post about Internationalization, I covered some non-obvious things that you should consider when adding translation capabilities to your code.

Today, let’s add to that by covering some non-obvious translation functions. You’re probably not using these, since they don’t get talked about as much. But there’s probably places where you should be using them, so knowing about them is the first step. And knowing is half the battle.

Basic functions, again

Last time I talked about these functions:

  • __()
  • _e()
  • _x()
  • _ex()
  • _n()

Let’s cover the ones I didn’t talk about.

Escaping output

In practice, you tend to use these mostly when outputting things onto the main page or in the admin. But, one thing you also use a lot when outputting text is the standard escaping functions. These are things like esc_html(), which outputs text in a way that makes it “safe” to go onto a webpage, without being interpreted as HTML. If the text comes from user input, then this is a good idea.

Now, if you think about it, then the text you have may be translated in some other file, which you don’t control either. So escaping that text might be a good idea too. If somebody snuck bad code into a translation file, a user might get bad things displayed without being able to easily find it.

So you could write something like echo esc_html(__('text','text-domain')), but that’s a bit wordy. Let’s talk about some shortcuts.

The esc_html__() function is the equivalent of esc_html(__(...)). It does the escaped html and the double-underscore translation all in one go. Similarly, the esc_html_e() function does the same thing, but it echoes the result, just like the _e() function would. And there’s also esc_html_x(), which is the equivalent of combining esc_html() and _x().

Along with those three are the three identical equivalents for attributes: esc_attr__(), esc_attr_e(), and esc_attr_x(). As the name implies, these combine the translation functions with esc_attr(), which is the escape function specifically intended when you’re outputting text into html attributes.

Also note there’s no shortcut for the equivalent of _ex(). It’s just not used that much, or at least not enough to need something special for it. Use an echo esc_html_x() instead.

There are no shortcuts for the other escaping functions as yet, but these can save a few keystrokes and make your code that much more readable.

The Numerical No-op

So we’ve got some shortcuts for escaping with those three functions, but where’s the love for _n()?

One of the problems with _n() is that it tends to require the strings to be in the same place that the PHP variable is. For all the other functions, you could have a big file of strings in an array, and then reference those strings by name or something elsewhere because they don’t require any PHP variables. Nothing about them is computed at the time of the output.

But not so with _n(), that $number to decide which string to use means that the strings have to be right there, they can’t be translated separately and referenced.

This is where _n_noop() comes in. The _n_noop() function basically takes the singular and plural strings for something, along with the text domain, and stores them in an array so that they can be referenced later by a function named translate_nooped_plural().

Perhaps an example is in order. Let’s go back to the tacos:

$string = sprintf( _n('You have %d taco.', 'You have %d tacos.', $number, 'plugin-domain'), $number );

What if we wanted those strings somewhere else? Like in a big file with all of our strings. Here’s a way to separate the strings from the _n() call:

$taco_plural = _n_noop('You have %d taco.', 'You have %d tacos.', 'plugin-domain');
$string = sprintf( translate_nooped_plural( $taco_plural, $number) , $number );

Now, that $taco_plural can be defined anywhere. Note that it contains no references to PHP variables. It’s basically static and unchanging. This allows us to separate it, then reference it elsewhere for the actual translation. The translate_nooped_plural() function performs the same job as _n() does, choosing which string to use based on the $number of tacos. The sprintf then pushes the $number into the chosen string, replacing the %d with the number.

Thus, that lets us extract the translatable strings out and put them anywhere we choose.

Also of note: The _nx_noop() function is a cross between _n_noop() and _x(). It takes a context for the translators as the third argument, and the domain becomes the fourth argument. Useful if you need to explain to the translators the context surrounding the pluralization choice.

Numbers and Dates

The number_format_i18n() function is functionally equivalent to the PHP number_format function. It lets you format numbers with commas at the thousands mark and so forth, except that it also takes localization into account. Not everybody uses commas for thousands and periods for decimals. This function will do the translation appropriately for that aspect.

The date_i18n() function is functionally equivalent to the PHP date function. It will handle all the same string formatting parameters as date() will, but it will cause output to be translated for month names, day-of-week names, and so forth. Of note is that it doesn’t change the format requested. If some places put days before months, for example, it won’t handle that. But it will output the month name in the native language (if the translation pack has the right month name in it). So you may want to run the date formatting string through __() as well, to let translators adjust the date format accordingly.

Wrap up

And that’s pretty much all the rest of the translation functions that I didn’t cover before. I may have forgotten a few useful ones here or there. Feel free to comment about anything I missed, or what you see most often, especially if you’re doing translations yourself.

Shortlink: