In the previous post I wrote a guide in how to create custom Gutenberg blocks with block.json. As a follow-up, this is a guide in how to handle translation of your block using this method.

With older methods we would use wp_set_script_translations() in order to set translated strings to a block – as detailed in my Create Custom Gutenberg Block – Part 8: Translation post. However the translation process is different if you are registering blocks using register_block_type_from_metadata() and block.json whereas you don’t manually enqueue the scripts . There are a few steps to know about, but it’s quite simple. Here’s how!

Define textdomain in block.json

The first step is defining textdomain in your block.json. Add a line with the property key “textdomain” and the translation slug you’re using as value. For example:

block.json
{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 2,
	"name": "awp/custom-block",
	"title": "AWP Custom Block",
	"description": "Example of custom block with block.json",
	"category": "design",
	"textdomain": "awp",
	"supports": { ... },
	"attributes": { ... },
	"editorScript": "file:build/index.js"
}

This means that everywhere you output custom strings you want translated in your block, you use the provided textdomain. In this example the textdomain is “awp”; so you would output strings like __( 'Settings', 'awp' ) in e.g. your edit.js file.

Generating a .pot file using WP CLI

The translation files generation process has not changed. You will still need to generate a .pot-file, translate it into a .po file (using e.g. PoEdit) which gets saved in a .mo-file, and then finally generate a .json file which contains all translations for your specific block. (Phew!)

You will need to use WP CLI (WordPress Command Line Interface) in order to generate the pot and json-files. How you access WP CLI differs from setup to setup, but the easiest method is to set up the commands in your package.json file (to run using npm) or composer.json if your plugin or theme has that. Or you can run the WP CLI command directly in terminal when needed.

In order to generate a .pot-file you use the WP CLI command wp i18n make-pot, with your desired parameters. For example:

wp i18n make-pot . languages/awp.pot --slug=awp --domain=awp --exclude=node_modules,src

The above command will look for strings with the textdomain “awp” in all files from the current directory and down – and generate the .pot file in a subfolder /languages/awp.pot. It also ignores the node_modules and src folders, which means it most likely should only look through in your final build folder. Take a look at WordPress’ documentation on wp i18n make-pot for more information and guidance.

After running the above command a .pot-file should be generated in the path you defined.

Now you can open for example PoEdit and create a new translation set from that .pot-file. Define the language you want to translate to and translate away! When you save you should have a .po file and a .mo-file.

Now’s the time to double check that all strings you’ve defined in your block files and block.json are included in this .pot-file! If there are strings missing, ensure that you’ve used the correct textdomain. If there are still missing strings, jump ahead to the section “It’s not working!” below.

When you’re done translating you will end up with a .mo-file which can be used for translating strings in PHP. If you have any of those, great – use load_plugin_textdomain() or the corresponding ones for theme in order to translate your PHP strings. However for translation in your Javascript files, there’s one more step.

Generating the .json translation file for your block

In order to define translated strings for Javascript/your block, you need a .json file. This .json-file can be generated from your .po-file using a WP CLI command. The command is simple:

wp i18n make-json languages/ --no-purge

Running from the current directory it will look into the subfolder languages/ for any .po-files to generate JSON files from. I highly recommend using the --no-purge option. Without this, this command will delete the translated strings from your .po-file – forcing you to re-translate them every time you need to regenerate translation files. Check out WordPress’ documentation on wp i18n make-json for more information and parameters.

The result from running this command should be one .json-file for each .po-file found in your languages folder. Its filename should be in the format of <textdomain>-<language code>-<hashcode>.json. The old method using wp_set_script_translation() would require your to rename the .json-file. But with the new method with block.json you leave the filename as it is!

With this .json-file and “textdomain” in your block.json, WordPress should automatically load your translation for your block! At least for the strings inside your block (for example in edit.js). But you will find that the block name, description, and any strings you’ve defined in block.json remains not translated. Simply follow the next steps!

Adding the generation commands to package.json or composer.json

To make it easier for yourself and other developers it’s a good idea to add your translation generation commands somewhere to be run more automatically – instead of off the top of your head in WP CLI every time your regenerate your build files.

Following the Creating Custom Gutenberg Blocks with block.json guide I added my block in a plugin, where I have a package.json for compiling my scripts. I can add the following two commands to “scripts“, like so:

package.json
{
	...
	"scripts": {
		"build": "wp-scripts build",
		"start": "wp-scripts start",
		"make-pot": "wp i18n make-pot . languages/awp.pot --slug=awp --domain=awp --exclude=node_modules,src",
		"make-json": "wp i18n make-json languages/ --no-purge"
	},
	...
}

With the above in your package.json you can now run the following commands to generate the .pot-file and the .json-file:

npm run make-pot
npm run make-json

Handling translation of strings within block.json

Following the steps above should give you a .json-file that should translate all strings using __(). However the strings in your block.json remains not translated. Unfortunately we have to add these manually to register_block_type_from_metadata(). This might change in the future, but as for the time of writing this (WordPress 5.8+) we need this one additional step.

In your register_block_type_from_metadata() function call, you need to provide parameters to the third argument: One line for each string in block.json you want translated. In my Creating Custom Gutenberg Blocks with block.json example I have block title and description in my block.json – which I want translated as well. I provide these values inside a PHP translation method, for example __() or _x(), like so:

plugin.php
add_action( 'init', 'awp_custom_block_init' );
function awp_custom_block_init() {
	register_block_type_from_metadata( __DIR__, [
		'render_callback' => 'awp_custom_block_render',
		'title'           => _x( 'Example Block', 'block title', 'awp' ),
		'description'     => _x( 'This is an example block using block.json', 'block description', 'awp' ),
	] );
}

Of course this means you need to register translation for PHP. For a plugin you’d use load_plugin_textdomain(). For example:

plugin.php
add_action( 'plugins_loaded', 'awp_custom_block_load_textdomain' );
function awp_custom_block_load_textdomain() {
	load_plugin_textdomain( 'awp', false, 'awp-custom-block/languages/' );
}

Check out the function documentation for more guidance if you get stuck on this.

With the above changes, your block should be fully translated! All strings within your block (e.g. edit.js) should be translated by the .json-file, and the strings in your block.json should be handled by parameters to register_block_type_from_metadata() and textdomain loaded in PHP.

But if you are one of those, myself included, who struggle with strings missing or translations not working, I have compiled a few things to investigate and possible fixes.

It’s not working!

If you’ve followed all the steps, ensuring there are no typos and WordPress is set to the language you have translated to, but your block is still not, or partly not, translated – read on. I’ve struggled with several issues and compiled a couple of possible fixes. Hopefully some of these steps work for you as well!

Issue #1: .json-file is not loaded

If your .json-file includes all the strings you have translated – but they are not loaded in Gutenberg, then you might be a victim of a temporary bug in WordPress. At the time of writing this, this is reported in this trac ticket. I do have a temporary fix for this – but be prepared to remove this once this bug gets corrected in WordPress core.

In short, we need to filter the paths to translation files (which WordPress believes is in /wp-content/languages/plugins) and correct the proper path for our block inside our plugin. In your plugin PHP file, add the following filter:

plugin.php
add_filter( 'load_script_translation_file', 'awp_custom_block_fix_translation_location', 10, 3 );
function fix_translation_location( string $file, string $handle, string $domain ): string {
	if ( strpos( $handle, 'awp-custom-block-editor-script' ) !== false && 'awp' === $domain ) {
		$file = str_replace( WP_LANG_DIR . '/plugins', plugin_dir_path( __FILE__ ) . 'languages', $file );
	}
	return $file;
}

Pay close attention to line #3. There’s two conditions you need to carefully edit to fit your block. The second one is easy – checking against the textdomain you’ve used all along. In this example it’s “awp“. The first condition searches for your block’s script handle. But if you’ve followed the Creating Custom Gutenberg Blocks with block.json guide you know we don’t manually enqueue the block script – and therefore we don’t define the script handle. So what is it then?

Your block’s handle enqueued through block.json will follow this pattern: <blockname but replace / with dash>-editor-script. In my example the block name (defined as “name” in block.json) is “awp/custom-block“. Convert all backlashes to dashes, and you get “awp-custom-block“. And then finally you append “-editor-script“.

With any luck, WordPress should now be able to navigate to the proper path and load your .json-file.

Issue #2: make-pot doesn’t find all strings

I have had several issues with WP CLI make-pot not finding all strings in my Javascript files. If you are in a situation where make-pot refuses to generate with strings defined in your Javascript files and/or block.json, you won’t be able to translate the strings, and naturally they will be missing from the .json-file as well, since this is generated from your .po-file. There’s a couple of things that worked for me: Ensure that you are using a fairly new version of WP i18n – both the WP CLI command, and the package:

Make sure to use newer wp-cli/i18n-command version

At the time of writing (WordPress 5.8+) I’ve had success with “wp-cli/i18n-command” version 2.2.13 and up. If your version is older, it might not be able to read your Javascript files. You can do this in terminal or in composer.json if you have that.

I’ve had success with adding this in “require” in my plugin’s composer.json:

composer.json
{
	...
	"require": {
		"wp-cli/i18n-command": "^2.2.13"
	}
}

Make sure to use newer @wordpress/i18n version

If you discover that none of the strings in your block.json file gets included in the generated .po-file, it might help to ensure to use a newer version of “@wordpress/i18n” package (I’ve had success with 4.3.1 and up). In your package.json, within “devDependencies“, add something like this:

package.json
{
	...
	"devDependencies": {
		...
		"@wordpress/i18n": "^4.3.1"
	}
}

Remember to run npm install after adding this to your package.json.

Then you might need to update the paths in your make-pot and make-json commands. This all depends on your setup, but there should be a (typically) vendor-folder somewhere where everything gets installed. Using my specific setup, I had to change the path to something like this:

package.json
"make-pot": "../../../vendor/bin/wp i18n make-pot . languages/awp.pot --slug=awp --exclude=node_modules,src",

When you run this “make-pot” command with npm, hopefully it should include all strings!