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:

40 Comments

  1. […] How to take advantage of WordPress 3.7′s language packs → […]

  2. Great article, I was looking for wp language packs. I have to install wordpress 3.7’s language pack.

  3. […] To get started making your themes and plugins be able to use these tools, check out Otto Wood’s guide. Language packs will be separated from WordPress core and maintained independently from core, […]

  4. So, if I’m following correctly, the only real changes needed for the time being are using the theme/plugin slug for textdomain, and adding the Text Domain: phpDoc header?

    We still need to reference the Theme/Plugin /languages/ directory for translation files when calling load_{theme/plugin}_textdomain(), right?

    • Related: at what point do Theme developers change translation file names from locale.mo/.po to textdomain-locale.mo/.po? Now, or at some point in the future?

    • Okay, attempting to answer my own question. If I use the following currently, then I’m good to go for support for both the current system *and* language packs?


      Text Domain: plugin-slug
      Domain Path: /languages
      load_plugin_textdomain( 'plugin-slug' );

    • You still need to reference where the language files are in the load_*_textdomain call, if you’re including them yourself.

      Theme developers will not change their translation file names at all. If you include your own pomo’s, then they remain just locale.mo. The language pack system will name them slug-locale.mo, but you won’t call them that or include them in the theme. The system will send them separately.

      Finally, if you leave off the directory in the load_* call, then you won’t be able to include your own pomo files, it will just use the language packs if and when they exist.

      This will become more clear as the structure gets built. For now, just ensure that you have the headers, that your text-domain is the slug, and that you’re calling the load_* function properly. Everything else will become clear over time.

  5. For a few plugins form wp.org I have my own special translation. I used to overwrite the plugin’s .mo file with this after each update. Recently I discovered that I could put them in wp-content/languages/plugins and just delete the plugins own translation.

    My question is: Is there a way to force the plugin(s) to (primarily) load the translation in wp-content/languages/plugins instead of the plugin’s own language folder? Is there a hook for this?

    • As you have discovered, the wp-content/languages/plugins directory is a new one to 3.7. That’s where the language packs will eventually go (for plugins).

      • So if language packs are automatically pulled from translate.wordpress.org (translated by someone) to folder wp-content/languages/plugins, where do I put my custom (heavily modified from original) translation file?

        • If you put it in the new location, and the plugin has it’s own, the plugin wins. So I guess we have to do as before, put our own in the plugins language folder and overwrite it each time the plugin is updated.

          I want a filter that allows a plugin to agnostic about the language folder and direct all translations to a custom folder.

          • You can always use the loading function: load_textdomain() which accepts own file paths. I do that all the time and create custom sub folders within wp-content/languages/ that won’t overridden on updates, also not for the new “language pack” system.

            These paths are used from auto updates/lang packs:
            wp-content/languages/plugins/*
            wp-content/languages/themes/*
            wp-content/plugins/your-plugin/*whatever (themes similar)

            so using wp-content/languages/your-plugin-slug-folder/ for example will not be touched on updates. You can use the function load_textdomain() or use the filter “override_load_textdomain” (don’t remember the correct name at the moment)

            We discussed this exact topic at WordCamp Europe with Otto & Nacin, and: custom lang files are just that, “custom”! So we have to care for the loading and path ourselves. However, using what I explained above will be future proof. Nacin promised me! The load_textdomain() function and that filter will stay!!!

  6. Hello Otto,

    How can I change the textdomain to my plugin slug, but still keep all the existing translation files?
    they’re hosted on a glotpress install

    • I’m not a GlotPress magician, so I don’t know the answer on that side of things. I’d imagine changing the slug in the database somewhere would work. Backup first, of course.

      For the plugin and the load call, just renaming the files to the new slug at the same time as the new plugin release happens should do the job.

    • You could for a certain amount of time let load both slugs/paths. Just with another load_*_textdomain() (or just load_textdomain()) call until all is setup for the new system and all users/translators have switched.

      WordPress could and can load more than one instance of a textdomain, those all will be merged. Not the most elegant way maybe, but possible, and in my opinion an alternative for managing the transition in such exact use cases.

  7. […] Language Packs 101 – Prepwork – Samuel Wood (for WordPress 3.7) […]

  8. To be sure – as I’m a bit confused from this post – the two lines about the text domain and the path are added to the header of the plugin file, correct? Not to the readme.txt?

  9. Hi Otto:

    Will these changes have any effect on plugins/themes that are NOT hosted in the .org repo?

    Custom internal plugins or ones sold in various places? Are any changes needed to those systems?

  10. I understand the Theme-slug = text_domain for theme and plugin. But what about widget ? It is usually treated like normal plugin – but the whete is the slug ?

  11. […] “has to”. I don’t mean to be fussy, and I’ll just say that this is really Otto’s fault, who has much higher standards than myself, all with his language packs and […]

  12. […] of individual strings (a few odd cases flashed by, but this post is long enough as it is), or even language packs. I strongly recommend you read Otto’s take on it all. He said it much better than I ever […]

  13. […] Make sure your theme is ready for language packs by following this essential guide from Otto: Language Packs 101 – Prepwork. […]

  14. Just reading this for the first time (thanks for the link Otto), but am I understanding this correctly? It may be very edge-case, but it appears there could be a possibility of a text-domain conflict if a plugin author and a theme author happen to have the same slug.

  15. Thanks very much for this great and helpfull writeup. Thanks! Whis you a goog new year!!
    jan

  16. Cool Article. Im actually putting together an il8n basic guide, so I’ve been reading around. I think the tip to use add-textdomain.php in preventive manner was really good advice, one of those things I may not have thought to do, only to later regret it. Thank you.

  17. […] If you need help adding support for translations and language packs, Sidler recommends Otto’s Language Packs 101 tutorial in addition to the Theme Developer Handbook section on […]

  18. […] those who have been following auto plugin translations, it has been in the works for years. It came as a welcome surprise in September when it was announced that plugins will finally be able […]

  19. You mention the makepot.php tool is able to scan a plugin’s directory and generate a .pot file. If I’m using the language pack exclusively, do I need to bother generating a .pot file at all?

  20. Hi Otto,
    I’ve used your “Internationalization: You’re probably doing it wrong” as the bible for my plugins for a long time. (I WAS doing it wrong!) But now that things have changed, I have a question I can’t find on the web. I really hope you can point me in the right direction (because quite frankly I don’t have a handle on the new polygots stuff yet).

    I use a shared library across most of my plugins. It uses a text domain of ‘mstw-loc-domain’, and my plugins now use their WP slugs as their text domains. I use ‘load_plugin_text_domain()’ with these strings on ‘plugins_loaded’, and no longer ship a /lang directory with the .po files. So how do I include the shared library in I18n? Can I make a second call to load_plugin_text_domain() with ‘mstw-loc-domain’? If so, I suppose I then have to include /lang with the .po file for the shared library? But does that then interfere with polygots translate.wordpress.org stuff for the plugin itself?

    Hope that makes sense.

    Thanks.

  21. […] is true especially since WordPress 3.7 and it got a whole lot easier since WordPress […]

  22. […] those who have been following auto plugin translations, it has been in the works for years. It came as a welcome surprise in September when it was announced that plugins will finally be able […]

Leave a Reply

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Need to post PHP code? Wrap it in [php] and [/php] tags.