How to Internationalize Your Block

WordPress is used all over the world and is translated into many non-english languages. Just like WordPress we need our plugins, themes, and blocks to provide that same access. Because internationalization (often referred to as i18n) is such an important aspect of WordPress, an API is exposed to help developers implement i18n features into their plugins and themes.

With the introduction of Gutenberg we need to bring i18n into our JavaScript code and WordPress has already provided the same API for us to use. I’m going to walk you through getting i18n setup for your Gutenberg block. I will not be going through how to use the i18n API’s WordPress provides but you can check out the official documentation linked below:

Getting started

In Getting Started with Block Development in ES.Next, we created a simple block with a minimal build process using webpack and Babel. We’re going to use that as a base to work from. Work through that article to get the base setup or grab the code from GitHub.

We are going to be using two utilities on the command line that will generate the proper .pot, .po, .mo, and .json files for our translations. You will need to install these if you haven’t already:

  • WP-CLI – The command line interface for WordPress. WP-CLI won’t need an installation of WordPress to use the commands we’ll be using.
  • gettext – internationalization and localization system used for writing multilingual programs. We will be utilizing the msgfmt utility that’s included with gettext.

Although we don’t have any translatable strings in our PHP code, we’re still going to setup and load our language files for the entire project.

Updating our starting point

From implementing i18n into my own projects I found that changing how the scripts and styles for our block are registered needs to change. Instead of hooking into enqueue_block_assets and enqueue_block_editor_assets we will utilize the register_block_type function within the init hook.

After removing the gwg_block_assets and gwg_editor_assets functions, we will create a new function named gwg_register_block_type with the following contents.

<?php
function gwg_register_block_type() {
if ( ! function_exists( 'register_block_type' ) ) {
// Gutenberg is not active.
return;
}
wp_register_style(
'gwg-style',
GWG_ESNEXT_PLUGIN_URL . 'style.css',
[],
GWG_ESNEXT_VERSION
);
wp_register_style(
'gwg-editor',
GWG_ESNEXT_PLUGIN_URL . 'editor.css',
[],
GWG_ESNEXT_VERSION
);
wp_register_script(
'gwg-block',
GWG_ESNEXT_PLUGIN_URL . 'block.build.js',
[ 'wp-blocks', 'wp-i18n' ],
GWG_ESNEXT_VERSION,
true // Enqueue script in the footer.
);
register_block_type(
'gwg/esnext-starter',
[
'editor_script' => 'gwg-block',
'editor_style' => 'gwg-editor',
'style' => 'gwg-style',
]
);
}
add_action( 'init', 'gwg_register_block_type' );
view raw 01.php hosted with ❤ by GitHub

In this function we first make sure that Gutenberg is active before registering the related styles and scripts. We then use the register_block_type to tell WordPress about the scripts and styles we’ve registered, if they should load in the editor or in the theme, and which block they belong to. It’s important to use register_block_type here so that when we tell WordPress about our translation files, the slugs will be registered in the background on time.

Make your strings translatable

We will be utilizing the @wordpress/i18n component which provides the i18n functions we’re used to in PHP but for JavaScript. Within our plugin’s index.php file, we’re going to update our wp_register_script call to include this dependency.

<?php
wp_register_script(
'gwg-block',
GWG_ESNEXT_PLUGIN_URL . 'block.build.js',
[ 'wp-blocks', 'wp-i18n' ],
GWG_ESNEXT_VERSION,
true // Enqueue script in the footer.
);
view raw 02.php hosted with ❤ by GitHub

Now we’ll use the __() (double-underscore) function to retrieve the translation of the text pass to it in our JavaScript code. First import the function from the i18n component accessed from the wp global variable in our block.js file.

const { __ } = wp.i18n;
view raw 03.js hosted with ❤ by GitHub

Next we’ll update all translatable strings in our code just like we would in PHP.

registerBlockType('gwg/esnext-starter', {
title: __('Get With Gutenberg - ESNext Starter', 'gwg'),
category: 'common',
edit(props) {
return <p className={props.className}>{__('Hello editor.', 'gwg')}</p>;
},
save(props) {
return <p className={props.className}>{__('Hello saved content.', 'gwg')}</p>;
},
});
view raw 04.js hosted with ❤ by GitHub

Generate the language files

Using WP-CLI we are going to generate our .pot file using the wp i18n make-pot command. This will search all files (excluding vendor/, node_modules/, and others for strings passed to our i18n functions with the defined text domain for our plugin and generate the .pot file used as a template for translators to use.

Check the documentation for more information about the available parameters if you’d like to customize it. I like to make sure the “Last-Translator” and “Language-Team” headers are passed into the generated file for reference. When using the WP-CLI command you can pass these headers as a json string to the --headers flag.

$ wp i18n make-pot ./ ./languages/gwg.pot --headers='{"Last-Translator":"JR Tashjian <jr@getwithgutenberg.com>","Language-Team":"Get With Gutenberg <info@getwithgutenberg.com>"}'

The WP-CLI command will create the langauges/ directory with our gwg.pot file within it. If you take a look at that file you will see references toblock.js file with our translatable strings.

#: block.js:5
msgid "Get With Gutenberg - ESNext Starter"
msgstr ""
#: block.js:9
msgid "Hello editor."
msgstr ""
#: block.js:13
msgid "Hello saved content."
msgstr ""
view raw 06.pot hosted with ❤ by GitHub

Now that we have our base translation file, let’s go ahead and add the en_US .po file since that is the locale I’m writing this for. All we have to do is copy our gwg.pot file to gwg-en_US.po.

$ cp ./languages/gwg.pot ./languages/gwg-en_US.po

Let’s make this easily repeatable by utilizing our npm scripts. Let’s make a new script called i18n-pot that we can run with npm run i18n-pot to automatically do the above two steps for us.

"scripts": {
"i18n-pot": "wp i18n make-pot ./ ./languages/gwg.pot --headers='{\"Last-Translator\":\"JR Tashjian <jr@getwithgutenberg.com>\",\"Language-Team\":\"Get With Gutenberg <info@getwithgutenberg.com>\"}' && cp ./languages/gwg.pot ./languages/gwg-en_US.po"
}
view raw 08.json hosted with ❤ by GitHub

We now have the translation file for our plugin to use. However this is only going to be used on the PHP side right now. The way the @wordpress/i18n component works is that it will read the data from a localized variable. This localized variable is generated by PHP when we load a .json file containing these translated strings through the wp_set_script_translations function in WordPress (available in version 5.0.0).

We’re going to use WP-CLI again to generate our .json file using the wp i18n make-json command. This command will extract the strings referenced in our build.js file from the gwg-en_US.po file and create our .json file.

$ wp i18n make-json languages/

This WP-CLI command will create a new .json file within our languages/ directory for every .po file within it. You’ll notice these files have an md5 hash appended to them which is a hash of the file path. We will change this because the path used for the md5 hash doesn’t match the one the wp_set_script_translations function in WordPress creates. This might be a bug or I might be missing something but after diving into the code I found renaming the file was a quick fix. We will rename the md5 hash portion of the filename to our enqueued script slug (gwg-block) which is the first file the function looks for.

$ mv languages/gwg-en_us-5bd7b3c720e0df736021f834799d5ef3.json languages/gwg-en_US-gwg-block.json

Again, let’s make this easily repeatable for us by adding a new script called i18n-json that we can run with npm run i18n-json. I’m going to add a call to the unix tool rename to rename all the md5 hashes our .json files have to gwg-block.

$ wp i18n make-json languages/ && rename 's/(gwg-[a-zA-Z_]+-)[^\.]*(\.json)/$1gwg-block$2/' ./languages/*

And here it is altogether in our package.json file.

"scripts": {
"i18n-json": "wp i18n make-json languages/ && rename 's/(gwg-[a-zA-Z_]+-)[^\\.]*(\\.json)/$1gwg-block$2/' ./languages/*"
}
view raw 12.json hosted with ❤ by GitHub

And finally the last portion of generating our translation files, the .mo files. We will use msgfmt to generate a .mo file for every .po file we have in the languages directory.

$ for file in `find . -name "*.po"` ; do msgfmt -o ${file/.po/.mo} $file ; done

And here it is in out package.json file.

"scripts": {
"i18n-mo": "for file in `find . -name \"*.po\"` ; do msgfmt -o ${file/.po/.mo} $file ; done"
}
view raw 14.json hosted with ❤ by GitHub

Now let’s add it all together into a single command. We’ve prefixed each command with i18n and I think that’s the perfect name to consolidate all the commands. We can then run every command with npm run i18n.

"scripts": {
"i18n": "npm run i18n-pot && npm run i18n-json && npm run i18n-mo"
}
view raw 15.json hosted with ❤ by GitHub

Putting it all together now

We have translated the strings in our code and we’ve generated the required files used to provide new translations to our project. We now only need to tell WordPress where these files are for our translations to load properly. We need to hook into the init action and call two functions in order for everything to work properly. First, we’ll add a new function to handle loading our translation files for PHP to use.

<?php
function gwg_init() {
load_plugin_textdomain( 'gwg', false, GWG_ESNEXT_PLUGIN_DIR . '/languages' );
}
add_action( 'init', 'gwg_init' );
view raw 16.php hosted with ❤ by GitHub

Now we can update our gwg_register_block_type function at the end with a call to wp_set_script_translations.

<?php
if ( function_exists( 'wp_set_script_translations' ) ) {
wp_set_script_translations( 'gwg-block', 'gwg', GWG_ESNEXT_PLUGIN_DIR . '/languages' );
}
view raw 17.php hosted with ❤ by GitHub

If you create a new post or page with Gutenberg and view the page source in your browser, search for gwg-esnext-i18n/block.build.js. You should see this snippet of code making your translated strings available to the @wordpress/i18n component.

<script type='text/javascript'>
( function( domain, translations ) {
var localeData = translations.locale_data[ domain ] || translations.locale_data.messages;
localeData[""].domain = domain;
wp.i18n.setLocaleData( localeData, domain );
} )( "gwg", {"translation-revision-date":"YEAR-MO-DA HO:MI+ZONE","generator":"WP-CLI\/2.1.0","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"en","plural-forms":"nplurals=2; plural=(n != 1);"},"Get With Gutenberg - ESNext Starter":[""],"Hello editor.":[""],"Hello saved content.":[""]}}} );
</script>
view raw 18.html hosted with ❤ by GitHub

Conclusion

There you have it. Internationalization is a part of plugin and theme development that should not be overlooked by developers. With only a little bit of additional work you can help bring your code to an entirely new audience and help others contribute to WordPress by providing translations.

I’ve put all the code for this walk-through up on Github for reference.

2 replies to “How to Internationalize Your Block”

  1. Oh dude Thanks, really.
    It was driving nuts. It didn’t worked just for one of my files (the other was just fine).
    I don’t know why, but renaming it without the md5 just worked fine.
    I’m digging further to understand the reason why.
    Thanks again!

    1. Ok so: md5 only works when the file is provided by wp.org directly.
      For plugins in repository, we don’t have to generate the files ourselves (but I still need to because wp.org can’t generate of my files).
      For the plugins in the repository, just use wp_set_script_translations() with only 2 arguments.
      don’t use load_plugin_textdomain().
      Don’t even make a languages folder

Comments are closed.