How to correctly internationalize WordPress child themes

Internationalization (i18n) and localization (l10n) are important and often overlooked aspects of WordPress development. Themes should give developers and administrators the ability to add translations for users from among the roughly 95% of the world whose first language isn’t English.

There are plenty of articles written about how to provide i18n and l10n for a single theme, but what about child themes? When building a child theme, assuming the parent theme is properly internationalized, developers may want to do any or all of the following:

  1. Provide localizations that aren’t provided by the parent theme, either in different languages or in gaps in existing localizations.
  2. Modify existing localizations in the parent theme, for instance if the developer of the child theme wants to change the wording of some area of the site.
  3. Provide new internationalizations unique to the child theme, using the child theme’s text domain.

Let’s start by looking at a simple example of i18n in a WordPress theme. For the purposes of this article, the parent theme will be called “Parent Theme” with the text domain parenttheme, and the child theme will be called “Child Theme” with the text domain childtheme.

Internationalization in templates

The author of a theme might include a line like this in a template file:

<?php
_e( 'Leave a reply', 'parenttheme' );
?>

This echos (_e) the string “Leave a reply,” while allowing for translations into other locales by targeting the text domain parenttheme. Let’s say, for instance, that a Mexican Spanish localization (provided by a file called es_MX.mo) translates this into “Deja una respuesta.” (Please forgive my use of Google Translate for this.) A user whose operating system is set to the es_MX locale will see this string instead of the standard “Leave a reply.”

(The theme developer also needs to include these lines in their functions.php file, assuming their localization files are kept in a /languages subdirectory:)

<?php
/**
 * parenttheme/functions.php
 */
function parenttheme_textdomain_setup() {
  // Load parent theme's l10ns for parent theme's i18ns
  load_theme_textdomain(
    'parenttheme',
    get_template_directory()  . '/languages'
  );
}
add_action( 'after_setup_theme', 'parenttheme_textdomain_setup' );
?>

Localizations on the parent theme, from child themes

Now let’s say we have a child theme whose developer wants to provide a different translation; say, for instance, they want to use the word “comment” instead of “reply.” They will create a new .mo localization file from the theme’s .pot file; but, of course, they don’t want to upload this into the parent theme’s languages directory, as that is likely to be overwritten every time the parent theme is updated.

(.pot files are provided by themes and plugins and tell translators what strings are available for localization; .mo files provide the localized strings themselves.)

Instead, they’ll want to keep this .mo file within the directory structure of the child theme, say in wp-content/childtheme/languages/parenttheme/es_MX.mo. Then they will need to load these localizations with something like this in the child theme’s functions.php:

<?php
/**
 * childtheme/functions.php
 */
function childtheme_textdomain_setup() {
  // Load child theme's l10ns for parent theme's i18ns
  load_child_theme_textdomain(
    'parenttheme',
    get_stylesheet_directory()  . '/languages/parenttheme'
  );
}
add_action( 'after_setup_theme', 'child_textdomain_setup' );
?>

Note four things here:

  1. We’re using the function load_child_theme_textdomain, not load_theme_textdomain. This allows us to add new or different localizations to any that are already provided by the parent theme, without removing all of them entirely.
  2. We’re specifying the parent theme’s text domain in this function; we’re saying, “Add localizations that target the parent theme’s text domain from this file, also.” This is because we’re still targeting template code that specifies the parenttheme text domain.
  3. We’re using get_stylesheet_directory(), not get_template_directory(); the former returns the path for the child theme, while the latter returns the path of the parent theme (even when called by the child theme).
  4. We’re using the directory languages/parenttheme inside of the child theme’s directory, rather than just languages. It will become clear why later.

At this point, our child theme can now provide its own localizations for the parent theme.

(Alternatively, the developer of the parent theme could have anticipated a child theme’s wanting to add its own l10n files and added this to the parent theme’s functions.php.)

Additional internationalizations from child themes

But what if our child theme adds new strings that are also internationalized? Say our child theme adds an author bio section, not present in the parent theme, with something like this:

<?php
/**
 * childtheme/template-parts/author.php, for instance
 */
_e( 'Author bio:', 'childtheme' );
?>

Now we need to provide localizations for two text domains, parenttheme and childtheme.

Each .mo file can only target a single text domain, so we’ll need separate .mo files here, one from each .pot file (the parent theme’s .pot file and the child theme’s .pot file).

To load localizations targeting the child theme’s text domain, we add a new line to our childtheme_textdomain_setup function:

<?php
/**
 * childtheme/functions.php
 */
function childtheme_textdomain_setup() {
  // Load child theme's l10ns for parent theme's i18ns
  load_child_theme_textdomain(
    'parenttheme',
    get_stylesheet_directory()  . '/languages/parenttheme'
  );

  // Load child theme's l10ns for child theme's i18ns
  load_theme_textdomain(
    'childtheme',
    get_stylesheet_directory()  . '/languages'
  );
}
add_action( 'after_setup_theme', 'child_textdomain_setup' );
?>

Here we’re adding the same function we saw before, load_theme_textdomain, for our child theme’s text domain, while using load_child_theme_textdomain for our parent theme’s text domain. (It can be a little confusing.)

Putting it all together

There! We’ve done it. We can now, within our child theme, provide localizations for internationalized strings in both our child theme and our parent theme. To summarize, in addition to creating the necessary .pot and .mo files, we’ve ensured we have these lines of code in the functions.php files of our parent and child themes:

<?php
/**
 * parenttheme/functions.php
 */
function parenttheme_textdomain_setup() {
  // Load parent theme's l10ns for parent theme's i18ns
  load_theme_textdomain(
    'parenttheme',
    get_template_directory()  . '/languages'
  );
}
add_action( 'after_setup_theme', 'parenttheme_textdomain_setup' );
?>
<?php
/**
 * childtheme/functions.php
 */
function childtheme_textdomain_setup() {
  // Load child theme's l10ns for parent theme's i18ns
  load_child_theme_textdomain(
    'parenttheme',
    get_stylesheet_directory()  . '/languages/parenttheme'
  );

  // Load child theme's l10ns for child theme's i18ns
  load_theme_textdomain(
    'childtheme',
    get_stylesheet_directory()  . '/languages'
  );
}
add_action( 'after_setup_theme', 'child_textdomain_setup' );
?>

Auto-generating .pot files

Finally, one more tip: creating .pot files from your theme is possible with Poedit, but requires a paid Pro edition, and requires that you regenerate the .pot file every time you add new internationalized strings to your theme. (To be clear, Poedit looks like terrific software, and application developers deserve to be compensated for their work, so if you like Poedit and want to continue to use it, consider buying it!) Instead you can automate this with gulp-wp-pot; just npm i --save-dev gulp-wp-pot and add this to your gulpfile.js:

var wpPot = require('gulp-wp-pot');

gulp.task('pot', function() {
  return gulp.src('./**/*.php')
    .pipe(wpPot( {
      domain: 'textdomain',
      package: 'themename'
    } ))
    .pipe(gulp.dest('languages/themename.pot'));
});

It’s customary to keep your theme’s .pot file in a languages subdirectory.

Supplementing theme (and plugin) localizations in wp-content

Though not quite within the original scope of this article, I discovered something while testing things.

It’s well-documented that plugin localizations can be overridden without any special preparation by site admins by putting .mo files into the wp-content directory: for instance, wp-content/languages/plugins/pluginname-es_MX.mo will supplement (or override) the localizations provided by the plugin itself. The advantage of this approach is that the contents of the wp-content are never overwritten by core, theme, or plugin updates.

However, I’ve seen it explicitly stated (on StackOverflow, I think), that the same can’t be done for theme localizations, and I have found this to be untrue. As a site admin who’s not developing themes, you can add a localization file with a directory and name such as wp-content/languages/themes/themename-es_MX.mo, and that localization file will supplement the existing localizations within the theme.

Please tell me where I screwed up

I’m positive I’ve almost certainly missed some things or gotten them wrong in everything above. Please leave a comment if you know of ways to improve this article. Thanks!

Leave a Reply