GeertDeDeckere.be

Loading WordPress language files the right way

March 18, 2012

WordPress provides a well-equipped toolbox for making plugins and themes translatable. However, you would be surprised to see how many plugins make it hard to properly add a translation for it. A setup for loading language files you will often encounter in WordPress plugins looks like this:

class My_Plugin {

    public function __construct()
    {
        load_plugin_textdomain('my-plugin', FALSE, dirname(plugin_basename(__FILE__)).'/languages/');
    }
}

$my_plugin = new My_Plugin;

As soon as the plugin file is loaded, the plugin constructor will be triggered. In there, load_plugin_textdomain() is called and gets told to load the applicable mo-files from the subdirectory “languages” of the plugin’s directory. That looks fine. You just drop your translation files in that directory and everybody is happy. Then the plugin gets updated and your custom translation is deleted. Ouch! Clearly, putting your own language files somewhere in the plugin’s directory is not a good idea.

A look at the source of the load_plugin_textdomain() function shows that after building the path to the mo-file, it simply calls the more general load_textdomain() function. That functions provides some useful hooks you can use to load your custom translation files without having to touch the plugin code.

Call load_plugin_textdomain() during init

The plugin setup shown above has one big problem, though. By the time you want to hook into the load_textdomain() function, in your theme’s functions.php file for example, it will be too late already since the load_plugin_textdomain() call gets done immediately when the plugin is loaded. That is why when developing a plugin you should call load_plugin_textdomain() during the init action and not earlier. Let’s fix that.

class My_Plugin {

    public function __construct()
    {
        add_action('init', array($this, 'load_plugin_textdomain'));
    }

    public function load_plugin_textdomain()
    {
        load_plugin_textdomain('my-plugin', FALSE, dirname(plugin_basename(__FILE__)).'/languages/');
    }
}

Now you have the chance to hook into load_textdomain(). Before we get there, it is worth noting that function calls for translatable strings, like __() or _e(), executed before the language files are loaded will be useless, as shown below. Be sure to bind the whole plugin initialisation to the init action to prevent this problem.

public function __construct()
{
    add_action('init', array($this, 'load_plugin_textdomain'));
    $text = __('I will not be translated!', 'my-plugin');
}

load_textdomain() hooks

Back to load_textdomain(). This functions provides three useful hooks, all of which get passed the language domain and path to the mo-file as arguments, albeit in different orders.

Filter: override_load_textdomain

This filter allows you to block the loading of a certain language file by returning TRUE. Even though this is not what we are looking for now, here is a quick example to block any language file that would get loaded from the plugin’s own “languages” directory. This code could go in your theme’s functions.php file.

add_filter('override_load_textdomain', 'block_my_plugin_default_language_files', 10, 3);

function block_my_plugin_default_language_files($override, $domain, $mofile)
{
    if ('my-plugin' === $domain && plugin_dir_path($mofile) === WP_PLUGIN_DIR.'/my-plugin/languages/')
        return TRUE;

    return $override;
}

Filter: load_textdomain_mofile

This filter allows you to change the path of the mo-file to load. This means that the original mo-file will not be loaded anymore. You basically replace the language file of the plugin with your own. This is a viable option, yet having the plugin’s default language file as a fallback is often a more preferable setup.

add_filter('load_textdomain_mofile', 'replace_my_plugin_default_language_files', 10, 2);

function replace_my_plugin_default_language_files($mofile, $domain)
{
    if ('my-plugin' === $domain)
        return WP_LANG_DIR.'/my-plugin/'.$domain.'-'.get_locale().'.mo';

    return $mofile;
}

Action: load_textdomain

This action looks like your best option. It allows you to load one or more additional language files on top of the plugin’s language files. The default language files of the plugin will be loaded after yours.

add_action('load_textdomain', 'load_custom_language_files_for_my_plugin', 10, 2);

function load_custom_language_files_for_my_plugin($domain, $mofile)
{
    // Note: the plugin directory check is needed to prevent endless function nesting
    // since the new load_textdomain() call will apply the same hooks again.
    if ('my-plugin' === $domain && plugin_dir_path($mofile) === WP_PLUGIN_DIR.'/my-plugin/languages/')
    {
        load_textdomain('my-plugin', WP_LANG_DIR.'/my-plugin/'.$domain.'-'.get_locale().'.mo');
    }
}

Load from the WordPress languages directory by default

The load_textdomain() functions allows for a lot of flexibility. Yet as a plugin developer you can make it even easier for the people using your plugin. By default you could look in the WordPress languages directory for translations of your plugin. That way custom translations would only need to be dropped in the subdirectory by the name of your plugin, in the WordPress languages directory. No need for the user to attach to any hooks in the code, and no problems during a plugin update.

Note that the default WordPress languages directory is “wp-content/languages/” but it can be overridden in wp-config.php by defining WP_LANG_DIR yourself.

public function load_plugin_textdomain()
{
    $domain = 'my-plugin';
    // The "plugin_locale" filter is also used in load_plugin_textdomain()
    $locale = apply_filters('plugin_locale', get_locale(), $domain);

    load_textdomain($domain, WP_LANG_DIR.'/my-plugin/'.$domain.'-'.$locale.'.mo');
    load_plugin_textdomain($domain, FALSE, dirname(plugin_basename(__FILE__)).'/languages/');
}

Finally, I would like to point out that is important to load custom user language files from WP_LANG_DIR before you load the language files that ship with the plugin. When multiple mo-files are loaded for the same domain, the first found translation will be used. This way the language files provided by the plugin will serve as a fallback for strings not translated by the user.

All in all by following a few practical guidelines you can make the loading of translations for your plugin a joy instead of a frustration.