Support

Account

Home Forums Gutenberg block.json with translated title and description (i18n & L10n)

Solved

block.json with translated title and description (i18n & L10n)

  • I thought it was a good idea to try to transition from the old PHP way of registering blocks to using the new block.json approach. However, I’m having difficulties to translate the strings for title and description. We usually do it like this (our blocks is part of our theme) in functions.php:

    acf_register_block_type(array(
    ...
    		'title'             => __('Special block', 'mytextdomain'),
    		'description'       => __('A block in great need of translations', 'mytextdomain'),
    ...
    	));

    And those strings get picked up by gettext which I usually run through poEdit to translate.

    Here’s how I do it now:
    functions.php

    add_action( 'init', 'register_acf_blocks', 5 );
    
    function register_acf_blocks() {
    	foreach ( glob( get_stylesheet_directory() . '/includes/blocks/*/' ) as $path ) {
    		register_block_type( $path . 'block.json' );
    	}
    }

    In my block.json I’ve specified my textdomain but from here I really don’t know how to pass those strings as gettext to show up in poEdit (which I use to translate my theme). Ideally I guess those strings should end up in my pot/po theme files.

    block.json

    {
    	"$schema": "https://schemas.wp.org/trunk/block.json",
    	"name": "acf/special-block",
    	"title": "Special block",
    	"description": "A block in great need of translation",
    	"category": "common",
    	"icon": "star-filled",
    	"apiVersion": 2,
    	"textdomain": "mytextdomain",
    	"keywords": [
    		"special",
    		"block",
    	],
    	"acf": {
    		"mode": "edit",
    		"renderTemplate": "special-block.php",
    		"postTypes": [ "page", "post" ]
    	},
    	"supports": {
    		"align": [ "full" ],
    		"mode": false,
    		"anchor": true,
    		"alignContent": false,
    		"color": {
    			"text": false,
    			"background": false,
    			"link": false
    		},
    		"alignText": false,
    		"spacing": {
    			"margin": false,
    			"padding": false
    		},
    		"typography": {
    			"lineHeight": false,
    			"fontSize": false
    		},
    		"fullHeight": false
    	}
    }

    I’ve Googled for hours and tried a bunch of things. I can extract the strings doing something like this, but it won’t work because I can’t pass a variable into gettext, I need the actual text (or so I think it works at least). If I use a function like this one below, can I in any way “print” the strings so it can be recognized by gettext?

    functions.php

    add_filter( 'block_type_metadata', 'site_block_type_metadata', 10 );
    function site_block_type_metadata( $metadata ) {
    	$name = $metadata['name'];
    	$name_parts = explode( '/', $name );
    	$handle = $name_parts[0];
    
    	if ( 'acf' === $handle ) {
    		/* This won't work since gettext can't get the value of variables */
    		$metadata['title'] = __( "{$metadata['title']}","textdomain");
    	}
    	return $metadata;
    }

    I’ve come across some articles using wp-cli to generate pot, po, json etc for a specific block (and using wp_set_script_translations() and so on). However that seems way overkill for only two strings per block (and also gives you separate translation files etc). Is there an easier way?

    Ps. I’m aware of this maybe is more of a “blocks in general” related question. But since we are encouraged to switch from the old nice ACF way of registering (acf_register_block_type()) our blocks maybe many more are, just like me, trying to figure this out. Also, I believe it to be easier to get good (and nice) replies on this forum. 🙂

  • I am running into the same problem. Did you find a solution with using Poedit?

  • No not yet. I have also tried using wp cli i18n to create .pot and json files (see below) and trying to register via:

    add_action( 'wp_enqueue_scripts', 'my_blocks_script_translations' );
    function my_blocks_script_translations() {
    	if ( function_exists( 'wp_set_script_translations' ) ) {
    		wp_set_script_translations( 'my-blocks', 'my-textdomain', get_template_directory() . '/includes/blocks/languages/' );
    	}
    }

    But no success yet.

    Problem 1: I can’t generate a json file when the string reference block.json is present in the .po file #: my-blocks/testblock/block.json. Like this:

    #: my-blocks/testblock/block.json
    #: my-blocks/google-maps/block.json
    msgctxt "block keyword"
    msgid "container"
    msgstr "behållare"

    Trying the same procedure but with the strings added in a .js file works, like #: my-blocks/testblock/script.js

    Problem 2: Even when using an online tool (such as wpeform.io/tools/make-po-to-json/) to convert my .po file to json I can’t get wp_set_script_translations() to work.

    Anyway, this is what I’ve tried (without success), maybe someone can figure things out which I can’t.

    functions.php: same snippet as above

    package.json:

    {
    	"name": "my-blocks",
    	"version": "1.0.0",
    	"description": "Builds pot and json files for translation of ACF blocks",
    	"author": "Me",
    	"scripts": {
    		"start": "wp-scripts start",
    		"build": "wp-scripts build",
    		"packages-update": "wp-scripts packages-update",
    		"make-pot": "wp i18n make-pot . languages/my-blocks.pot --exclude=node_modules,src --ignore-domain --headers='{\"Last-Translator\":\"lepardman <[email protected]>\",\"Language-Team\":\"The team <[email protected]>\"}'",
    		"make-json": "wp i18n make-json languages/ --no-purge --pretty-print"
    	},
    	"dependencies": {
    		"@wordpress/i18n": "^4.27.0"
    	},
    	"devDependencies": {
    		"@wordpress/scripts": "^25.4.0"
    	}
    }

    I first run wp i18n make-pot to get a pot file (this works, all strings are added). Then I use PoEdit to create a po file with translations (or copy+paste pot and change file name to .po and translate). And lastly I run wp i18n make-json but it creates no files (output: “Success: Created 0 files”). See problem #1.

  • Hi lepardman,

    Thank you for your response! I will take a deep dive into the translation part this week since I am in the middle to convert my library of blocks to the ACF v2 blocks. I will try to debug your solution or find another one. If I have any success, I will let you know!

  • Update! I’ve stumbled across this article saying that it can’t be done like any of the methods above for the time being. Instead you’d have to add the strings in an extra step and registering the blocks with register_block_type_from_metadata() instead of register_block_type().

    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.

    My previous function looked like this:

    function register_acf_blocks() {
    	foreach ( glob( get_stylesheet_directory() . '/includes/blocks/*' ) as $path ) {
    		register_block_type( $path . '/block.json' );
    	}
    }

    Then I tried getting and using the strings from the block.json files, but without success (I guess the same problem as in my first post. You can’t pass a variable into a gettext function).

    add_action( 'init', 'register_acf_blocks', 5 );
    function register_acf_blocks() {
    	foreach ( glob( get_stylesheet_directory() . '/includes/blocks/*' ) as $path ) {
    		$str = file_get_contents( $path . '/block.json' );
    		$json = json_decode($str, true);
    		$title = $json['title'];
    		$desc = $json['description'];
    
    		register_block_type_from_metadata( $path . '/block.json', [
    			'title' => _x( $title, 'block title', 'mytextdomain' ),
    			'description' => _x( $desc, 'block description', 'mytextdomain' ),
    		] );
    	}
    }

    So then I guess we’re left with the manual and double declaration way, as the article above states.

    add_action( 'init', 'register_acf_blocks', 5 );
    function register_acf_blocks() {
    	$path = get_stylesheet_directory() . '/includes/blocks/';
    
    	register_block_type_from_metadata( $path . 'my-block-1/block.json', [
    		'title' => _x( 'Title text of block #1', 'block title', 'theme-textdomain' ),
    		'description' => _x( 'This is the description of block #1', 'block description', 'theme-textdomain' ),
    	] );
    
    	register_block_type_from_metadata( $path . 'my-block-2/block.json', [
    		'title' => _x( 'Title text of block #2', 'block title', 'theme-textdomain' ),
    		'description' => _x( 'This is the description of block #2', 'block description', 'theme-textdomain' ),
    	] );
    
    	register_block_type_from_metadata( $path . 'my-block-3/block.json', [
    		'title' => _x( 'Title text of block #3', 'block title', 'theme-textdomain' ),
    		'description' => _x( 'This is the description of block #3', 'block description', 'theme-textdomain' ),
    	] );
    
    }

    Voilà! It works!

    A good thing about this is that I can use the theme’s textdomain (using a custom textdomain for your blocks would need a separate load_theme_textdomain() function in functions.php and creating .pot/.po files), allowing me to only have one .po file for all theme related strings and also no need for anything wp-cli i18n, npm etc. The downside is that you’d have to register each block manually and also handle double title and description texts (not sure if you can omit them from block.json).

    To make it more modular I can add register_block_type_from_metadata() in a separate php file (like setup.inc.php) in each block folder and then use the second approach above, looping through all folders and include the php file instead. That way you wouldn’t need to edit function.php for adding/removing blocks.

    functions.php:

    add_action( 'init', 'register_acf_blocks', 5 );
    function register_acf_blocks() {
    	global $path;
    
    	foreach ( glob( get_stylesheet_directory() . '/includes/blocks/*' ) as $path ) {
    		include $path . '/setup.inc.php';
    	}
    
    }

    In each of my block folders I add a new file setup.inc.php:

    <?php
    register_block_type_from_metadata( $path . '/block.json', [
        'title' => _x( 'Title of this block', 'block title', 'theme-textdomain' ),
        'description' => _x( 'This is the description of this block', 'block description', 'theme-textdomain' ),
    ] );

    Now, I’m no developer, more of an UI/UX guy. So this can probably be written even better. But I think this approach should be reviewed by the ACF devs and maybe added to the docs as an alternative to register_block_type() if this is the only way to handle translations of block.json strings. Since there is a vast amount of non-english installs of WP and by using ACF we actively want to build custom blocks with PHP (= .po) rather than JS. 🙂

  • Update! I’ve stumbled across this article saying that it can’t be done like any of the methods above for the time being. Instead you’d have to add the strings in an extra step and registering the blocks with register_block_type_from_metadata() instead of register_block_type().

    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.

    My previous function looked like this:

    function register_acf_blocks() {
    	foreach ( glob( get_stylesheet_directory() . '/includes/blocks/*' ) as $path ) {
    		register_block_type( $path . '/block.json' );
    	}
    }

    I changed register_block_type to register_block_type_from_metadata() and then tried getting and using the strings from the block.json files, but without success (I guess the same problem as in my first post. You can’t pass a variable into a gettext function).

    add_action( 'init', 'register_acf_blocks', 5 );
    function register_acf_blocks() {
    	foreach ( glob( get_stylesheet_directory() . '/includes/blocks/*' ) as $path ) {
    		$str = file_get_contents( $path . '/block.json' );
    		$json = json_decode($str, true);
    		$title = $json['title'];
    		$desc = $json['description'];
    
    		register_block_type_from_metadata( $path . '/block.json', [
    			'title' => _x( $title, 'block title', 'mytextdomain' ),
    			'description' => _x( $desc, 'block description', 'mytextdomain' ),
    		] );
    	}
    }

    So then I guess we’re left with the manual and double declaration way, as the article above states.

    add_action( 'init', 'register_acf_blocks', 5 );
    function register_acf_blocks() {
    	$path = get_stylesheet_directory() . '/includes/blocks/';
    
    	register_block_type_from_metadata( $path . 'my-block-1/block.json', [
    		'title' => _x( 'Title text of block #1', 'block title', 'theme-textdomain' ),
    		'description' => _x( 'This is the description of block #1', 'block description', 'theme-textdomain' ),
    	] );
    
    	register_block_type_from_metadata( $path . 'my-block-2/block.json', [
    		'title' => _x( 'Title text of block #2', 'block title', 'theme-textdomain' ),
    		'description' => _x( 'This is the description of block #2', 'block description', 'theme-textdomain' ),
    	] );
    
    	register_block_type_from_metadata( $path . 'my-block-3/block.json', [
    		'title' => _x( 'Title text of block #3', 'block title', 'theme-textdomain' ),
    		'description' => _x( 'This is the description of block #3', 'block description', 'theme-textdomain' ),
    	] );
    
    }

    Voilà! It works!

    A good thing about this is that I can use the theme’s textdomain (using a custom textdomain for your blocks would need a separate load_theme_textdomain() function in functions.php and creating .pot/.po files), allowing me to only have one .po file for all theme related strings and also no need for anything wp-cli i18n, npm etc. The downside is that you’d have to register each block manually and also handle double title and description texts (not sure if you can omit them from block.json).

    To make it more modular I can add register_block_type_from_metadata() in a separate php file (like setup.inc.php) in each block folder and then use the second approach above, looping through all folders and include the php file instead. That way you wouldn’t need to edit function.php for adding/removing blocks.

    functions.php:

    add_action( 'init', 'register_acf_blocks', 5 );
    function register_acf_blocks() {
    	global $path;
    
    	foreach ( glob( get_stylesheet_directory() . '/includes/blocks/*' ) as $path ) {
    		include $path . '/setup.inc.php';
    	}
    
    }

    In each of my block folders I add a new file setup.inc.php:

    <?php
    register_block_type_from_metadata( $path . '/block.json', [
        'title' => _x( 'Title of this block', 'block title', 'theme-textdomain' ),
        'description' => _x( 'This is the description of this block', 'block description', 'theme-textdomain' ),
    ] );

    Now, I’m no developer, more of an UI/UX guy. So this can probably be written even better. But I think this approach should be reviewed by the ACF devs and maybe added to the docs as an alternative to register_block_type() if this is the only way to handle translations of block.json strings. Since there is a vast amount of non-english installs of WP and by using ACF we actively want to build custom blocks with PHP (= .po) rather than JS. 🙂

  • Hi! I tried your solution but I found an easier way! For this to work you should have wp-cli installed on your machine. The wp-cli commandos I list here I run through my package.json which makes it even easier.

    Step 1.
    Make sure your block.json files have the text domain you use in your theme/plugin as well.

    Example block.json file, note the textdomain.

    {
        "$schema": "https://schemas.wp.org/trunk/block.json",
        "name": "acf/accordion",
        "title": "Accordion",
        "description": "Accordion (formerly known as FAQ)",
        "category": "wp-lemon-blocks",
        "icon": "lightbulb",
        "keywords": ["FAQ", "frequently", "asked", "questions", "accordion", "toggle", "collapsible", "expand", "collapse"],
        "apiVersion": 2,
        "style": "file:./index.css",
        "script": "file:./index.js",
        "textdomain": "wp-lemon",
        "acf": {
            "mode": "preview"
        },
        "supports": {
            "mode": false
        }
    }
    

    Step 2.
    Run the following command to create your pot translation base file. Make sure you set the path correctly to your preffered translation directory and have the name of the file the same as your text domain.

    wp i18n make-pot . resources/languages/wp-lemon.pot --slug=wp-lemon --domain=wp-lemon --exclude=node_modules,vendor

    Step 3.
    Create a translation file from the pot file. I used Poedit to create a nl_NL.po file in my resources/languages/ directory. Translate some first string to test. Once you save your translated .po file, poedit will also create a .mo file

    Step 4.
    Load your translation files. I added the following in my functions.php

    function theme_initialize()
    {
    	load_theme_textdomain('wp-lemon', get_template_directory() . '/resources/languages/');
    }
    add_action('after_setup_theme', 'theme_initialize');

    Step 5.
    Profit?

    Additional
    If you update a string or add a new block for example, run the make-pot command first and the update-po afterwards. Now run poedit on your po files and translate the files again. Make sure the .mo files are updated as well on save.

Viewing 7 posts - 1 through 7 (of 7 total)

You must be logged in to reply to this topic.