Do Child-Themes automatically load the Translation from the Parent-Theme?

Is it enough to just create a child-theme – let’s say technically without adding anything else but the bare minimum style.css – to have the translation of the parent-theme being used automatically for the child-theme as well?

Basically, the answer is NO, … but… there’s an option:

Add a mu-plugin.

This (MU-)Plugin does several things:

  1. It hooks into after_setup_theme on a priority of 20 – assuming that the parent textdomain/i18n .mo file is loaded correctly at the default priority on the correct hook.
  2. Then it retrieves an instanceof the WP_Theme – in this case the child theme.
  3. Then it checks if there actually is a child theme in use.
  4. If this was true, then it will just load the textdomain from the parent file.

It’s actually quite easy, as the core class does a lot of checks for us: It retrieves another instance of WP_Theme for the parent theme. Then it checks if the TextDomain header is set, using: $current_theme->get( 'TextDomain' );. The point therefore is there comes one convention into the game: This plugin will only work, if the parent theme got a Text Domain and(!) a Domain Path header set.

<?php
/**
 * Plugin Name: (#113391) Parent Theme i18n Autoloader
 * Description: Load Twenty12 Child theme translation files automagically from Parent
 */
add_action( 'after_setup_theme', 'wpse113391_parent_theme_i18n_autoloader', 20 );
function wpse113391_parent_theme_i18n_autoloader()
{
    $current_theme = wp_get_theme();
    if ( is_child_theme() )
        $current_theme->parent()->load_textdomain();
}

Now here comes the problem: The default/standard Twenty* themes delivered by core do not(!) have the Domain Path header entry. And this is something that we have to fix instantly, as load_theme_textdomain() else searches for the translation file not in the Parent theme folder, but

  • first in the child theme folder: get_stylesheet_directory().WP_Theme::get( 'DomainPath' ), which means that (A) the Domain Path needs to be set and it needs to be prefixed with a slash: /.
  • then in the child theme folder: `get_stylesheet_directory().’/languages’.
  • and last in the in WP_LANGUAGE_DIR.'/themes' directory.

Note: I guess that’s just a bug that will never be fixed for “backwards compatibility”, which – in other words – means that there is a bug, but there may be devs already working around it. 😛

Then there’s another problem. The WP_Theme class method load_textdomain() internally passes a $path to load_theme_textdomain(). And this parameter is $this->get_stylesheet_directory(). And this method returns $this->theme_root . "https://wordpress.stackexchange.com/" . $this->stylesheet. So the function would actually work quite good, but it’s messing it up with simply calling an internal replacement for get_stylesheet_directory() (which would have been filterable). One might now think

“Hey! The class implements ArrayAccess! So simply set the missing array key of Domain Path!”

Wrong. All the class properties are marked private and inaccessible.

Then you might think

“Why not simply extend the WP_Theme class and define a set() Method so we can manually set the missing header entries?”

Wrong. The class itself is final and not extensible.

Result: We’re left with what load_theme_textdomain() – the last function in the chain of calls – offers us. Now we got a larger plugin that intercepts the load_theme_textdomain() call to load the correct file. To not disturb other i18n file loads, it instantly removes the callback from the filter to keep your environment tidy.

<?php
/**
 * Plugin Name: (#113391) Parent Theme i18n Autoloader
 * Description: Load Twenty12 Child theme translation files automagically from Parent
 */

add_action( 'muplugins_loaded', array( 'WPSE113391Parenti18nLoader', 'getInstance' ) );
class WPSE113391Parenti18nLoader
{
    public static $instance = null;

    private $theme = null;

    public static function getInstance()
    {
        null === self::$instance AND self::$instance = new self;
        return self::$instance;
    }

    public function __construct()
    {
        add_action( 'after_setup_theme', array( $this, 'i18nAutoloader' ), 20 );
    }

    public function setTheme( $theme )
    {
        return $this->theme = $theme;
    }

    public function getTheme()
    {
        return $this->theme;
    }

    public function i18nAutoloader()
    {
        if ( ! is_child_theme() )
            return;

        $current_theme = wp_get_theme();
        if ( '' === $current_theme->parent()->get( 'DomainPath' ) )
        {
            $this->setTheme( $current_theme->parent() );
            add_filter( 'override_load_textdomain', array( $this, 'overrideI18nLoader' ), 10, 3 );
        }
        $current_theme->parent()->load_textdomain();
    }

    public function overrideI18nLoader( $activate, $domain, $mofile )
    {
        // Don't intercept anything else: Self removing
        remove_filter( current_filter(), __FUNCTION__ );

        // Rebuild the internals of WP_Theme::get_stylesheet_directory() and load_theme_textdomain()
        $theme  = $this->getTheme();
        $path   = trailingslashit( $theme->get_theme_root() ).$theme->get_template();
        $locale = apply_filters( 'theme_locale', get_locale(), $domain );

        load_textdomain( $domain, "{$path}/{$locale}.mo" );

        // Return true to abort further attempts
        return true;
    }
}

Leave a Comment