Block themes translate WordPress differently from a typical approach. Traditional PHP template files with translation functions don’t work with HTML templates, JavaScript-powered blocks, and the Site Editor. This shift requires you to understand WordPress Block internationalization systems differently.
This guide provides strategies for making your Block themes multilingual. You learn how to navigate the challenges of Block theme translation, implement solutions, and integrate with translation plugins.
Why Block themes break traditional translation methods (and how to fix them)
Block themes replace many of WordPress’ PHP files with HTML templates that contain Block markup. However, this switch creates challenges because HTML templates cannot execute PHP translation functions such as _()
or _e()
. As a result, the translation strings you already have sit uselessly in static files.
WordPress 6.8 brings some improvements that simplify Block theme internationalization. Primarily, themes with proper Text Domain and Domain Path headers) no longer need manual load_theme_textdomain()
calls.
Instead, WordPress auto-loads translation files and prioritizes wp-content/languages/themes/
over theme directories for performance.
To begin, set up your theme using a classic approach by adding metadata to the style.css
file.
/*
Theme Name: My Block Theme
Text Domain: my-block-theme
Domain Path: /languages
*/
Note that the Text Domain header must match your theme’s folder name (usually in kebab-case) to ensure translation files auto-load correctly in recent WordPress versions.
Likewise to style.css
, your functions.php
file requires minimal setup:
<?php
// Optional in WordPress 6.8+ but included for backward compatibility
function my_block_theme_setup() {
load_theme_textdomain( 'my-block-theme', get_template_directory() . '/languages' );
}
add_action( 'after_setup_theme', 'my_block_theme_setup' );
// Register block scripts with translation support
function my_block_theme_scripts() {
wp_enqueue_script(
'my-block-theme-scripts',
get_template_directory_uri() . '/assets/js/theme.js',
array( 'wp-i18n' ),
'1.0.0',
true
);
wp_set_script_translations(
'my-block-theme-scripts',
'my-block-theme',
get_template_directory() . '/languages'
);
}
add_action( 'wp_enqueue_scripts', 'my_block_theme_scripts' );
The key difference between classic and Block themes here is that the latter splits translation responsibility between server-side PHP and client-side JavaScript. In contrast, classic themes have to rely on PHP to handle most translations.
How to build block.json translations
The block.json file is your ‘configuration hub’ for the Block you wish to translate. Setting up proper internationalization ensures that your Blocks translate correctly both in the editor and on the front-end.
The canonical way to register a Block is through block.json
. Starting with the textdomain
configuration means WordPress can translate the title, description, and keywords fields when the textdomain
is set:
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "my-theme/testimonial",
"title": "Testimonial",
"category": "text",
"description": "Display customer testimonials",
"keywords": ["quote", "review", "testimonial"],
"textdomain": "my-block-theme",
"attributes": {
"content": {
"type": "string",
"source": "html",
"selector": "blockquote"
}
}
}
However, scenarios requiring ‘context’ need server-side registration. Context, in this case, matters because the same word might translate differently based on its usage. For example, “post” as a noun versus as a verb requires different translations in many languages:
function my_theme_register_testimonial_block() {
register_block_type_from_metadata(
get_template_directory() . '/blocks/testimonial',
array(
'title' => _x( 'Testimonial', 'block title', 'my-block-theme' ),
'description' => _x(
'Display customer testimonials',
'block description',
'my-block-theme'
),
'keywords' => array(
_x( 'quote', 'block keyword', 'my-block-theme' ),
_x( 'review', 'block keyword', 'my-block-theme' )
)
)
);
}
add_action( 'init', 'my_theme_register_testimonial_block' );
Any Block variations you include need structured naming, too, because WordPress looks for specific patterns when loading your translations. Each variation name becomes part of the translation key:
{
"name": "my-theme/button",
"title": "Button",
"textdomain": "my-block-theme",
"variations": [{
"name": "primary",
"title": "Primary Button",
"attributes": {
"className": "is-style-primary"
}
},
{
"name": "secondary",
"title": "Secondary Button",
"attributes": {
"className": "is-style-secondary"
}
}
]
}
JavaScript internationalization requires you to import WordPress i18n functions and configure the script translations. This is because the Site Editor runs in the browser rather than on the server. Because PHP translation functions don’t exist in JavaScript, WordPress provides equivalent functions through the @wordpress/i18n
package:
import {
registerBlockType
} from '@wordpress/blocks';
import {
__
} from '@wordpress/i18n';
import {
useBlockProps,
RichText
} from '@wordpress/block-editor';
registerBlockType('my-theme/testimonial', {
edit: ({
attributes,
setAttributes
}) => {
const blockProps = useBlockProps();
return ( < div { ...blockProps } >
< RichText tagName = "blockquote" value = { attributes.content } onChange = { (content) => setAttributes({
content
})
}
placeholder = {
__('Add testimonial text...', 'my-block-theme')
}
/> < cite >
< RichText tagName = "span" value = { attributes.author } onChange = { (author) => setAttributes({
author
})
}
placeholder = {
__('Author name', 'my-block-theme')
}
/> < /cite> < /div>
);
}
});
In addition, it’s a good idea to generate JSON translation files for JavaScript because WordPress uses a different format for client-side translations. PHP uses .mo
files, but JavaScript needs .json
files with specific naming conventions. You can automate this using WP-CLI commands:
# Extract strings from JavaScript files into POT
wp i18n make-pot . languages/my-block-theme.pot
# Convert PO files to JSON for JavaScript
wp i18n make-json languages/ --no-purge --pretty-print
The resulting JSON files follow a consistent pattern: {textdomain}-{locale}-{handle}.json
. WordPress can then load these when you call wp_set_script_translations()
.
Converting your static HTML templates into translation-ready PHP components
Given that HTML templates are static, working with them for Block theme internationalization is challenging, as your existing translation functions and techniques won’t work.
PHP-powered template parts could solve this problem because WordPress processes them as PHP files despite being referenced in HTML templates. This hybrid approach maintains the Block theme structure while enabling dynamic content:
<!-- templates/page.html -->
<!-- wp:template-part {"slug":"header","tagName":"header"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
<!-- wp:post-title {"level":1} /-->
<!-- wp:post-content /-->
<!-- wp:template-part {"slug":"post-meta"} /-->
</main>
<!-- /wp:group →
<!-- wp:template-part {"slug":"footer","tagName":"footer"} /-->
Note that the template part can contain PHP:
<!-- parts/post-meta.html -->
<!-- wp:group {"className":"post-meta"} -->
<div class="wp-block-group post-meta">
<?php
echo sprintf(
/* translators: 1: Post date, 2: Post author */
__( 'Published on %1$s by %2$s', 'my-block-theme' ),
get_the_date(),
get_the_author()
);
?>
</div>
<!-- /wp:group -->
Complex blocks need the render.php
file because some content requires server-side processing that Block attributes alone cannot handle. Database queries, conditional logic, and dynamic content generation all require PHP execution:
// blocks/recent-posts/render.php
<?php
$recent_posts = get_posts( array(
'numberposts' => $attributes['count'] ?? 5
) );
?>
<div <?php echo get_block_wrapper_attributes(); ?>>
<h3><?php echo esc_html__( 'Recent Posts', 'my-block-theme' ); ?></h3>
<?php if ( $recent_posts ) : ?>
<ul>
<?php foreach ( $recent_posts as $post ) : ?>
<li>
<a href="<?php echo get_permalink( $post ); ?>">
<?php echo get_the_title( $post ); ?>
</a>
<span class="post-date">
<?php echo get_the_date( '', $post ); ?>
</span>
</li>
<?php endforeach; ?>
</ul>
<?php else : ?>
<p><?php esc_html_e( 'No posts found.', 'my-block-theme' ); ?></p>
<?php endif; ?>
</div>
This means configuring your Block to use the render file in block.json
:
{
"name": "my-theme/recent-posts",
"render": "file:./render.php",
"attributes": {
"count": {
"type": "number",
"default": 5
}
}
}
How to implement dynamic content translation for custom fields and user input
Despite its prevalence on WordPress websites, dynamic content can cause translation issues because it exists in the database rather than your theme’s files. As such, any third-party translation plugins you use need to identify and manage this content separately from static theme strings.
This is where registering custom fields with proper meta configuration is valuable because translation plugins hook into WordPress’ meta system to detect any translatable content. The show_in_rest
parameter enables compatibility with the Site Editor:
function my_theme_register_meta_fields() {
register_post_meta( 'page', 'custom_subtitle', array(
'type' => 'string',
'description' => __( 'Page subtitle', 'my-block-theme' ),
'single' => true,
'show_in_rest' => true,
'auth_callback' => function() {
return current_user_can( 'edit_posts' );
}
));
}
add_action( 'init', 'my_theme_register_meta_fields' );
// Display with plugin compatibility
function my_theme_display_subtitle( $post_id ) {
$subtitle = get_post_meta( $post_id, 'custom_subtitle', true );
if ( ! $subtitle ) {
return;
}
// WPML compatibility
// (documented at wpml.org/documentation/support/wpml-coding-api/wpml-hooks-reference/)
if ( function_exists( 'icl_t' ) ) {
$subtitle = icl_t(
'my-block-theme',
'subtitle_' . $post_id,
$subtitle
);
}
// Polylang compatibility
// (documented at polylang.pro/doc/function-reference/)
if ( function_exists( 'pll_translate_string' ) ) {
$subtitle = pll_translate_string( $subtitle, 'my-block-theme' );
}
echo '<h2 class="page-subtitle">' . esc_html( $subtitle ) . '</h2>';
}
Database queries also need language filtering because WordPress doesn’t automatically filter content by language. Translation plugins add query modifications that you have to accommodate for:
function my_theme_get_localized_posts( $args = array() ) {
$defaults = array(
'post_type' => 'post',
'posts_per_page' => 10
);
$args = wp_parse_args( $args, $defaults );
// Polylang adds language taxonomy
// (documented at polylang.pro/doc/developpers-how-to/)
if ( function_exists( 'pll_current_language' ) ) {
$args['lang'] = pll_current_language();
}
// WPML filters queries automatically when suppress_filters is false
// (wpml.org/documentation/getting-started-guide/translating-custom-posts/)
if ( defined( 'ICL_LANGUAGE_CODE' ) ) {
$args['suppress_filters'] = false;
}
return get_posts( $args );
}
Your form processing mixes dynamic and static content, but form labels, error messages, and admin notifications all need language-aware translation. The email recipients might also vary by language:
function my_theme_process_contact_form() {
if ( ! isset( $_POST['contact_nonce'] ) ||
! wp_verify_nonce( $_POST['contact_nonce'], 'contact_form' ) ) {
return;
}
$name = sanitize_text_field( $_POST['name'] );
$email = sanitize_email( $_POST['email'] );
$message = sanitize_textarea_field( $_POST['message'] );
// Get admin email in current language
$admin_email = get_option( 'admin_email' );
// For language-specific admin emails, use WPML's string translation
// (documented at wpml.org/documentation/support/wpml-coding-api/wpml-hooks-reference/)
if ( function_exists( 'icl_t' ) ) {
// First register the string if not already registered
if ( function_exists( 'icl_register_string' ) ) {
icl_register_string( 'my-block-theme', 'contact_email', $admin_email );
}
$admin_email = icl_t(
'my-block-theme',
'contact_email',
$admin_email
);
}
$subject = sprintf(
/* translators: %s: Sender name */
__( 'Contact form submission from %s', 'my-block-theme' ),
$name
);
wp_mail( $admin_email, $subject, $message );
}
add_action( 'init', 'my_theme_process_contact_form' );
It’s also important to assess your navigation language awareness because menu items, URLs, and structure might differ between languages. Your translation plugin likely has an API for building language switchers:
function my_theme_language_switcher_block() {
if ( ! function_exists( 'pll_the_languages' ) &&
! function_exists( 'icl_get_languages' ) ) {
return;
}
$output = '<div class="language-switcher">';
// Polylang language switcher
// (documented at polylang.pro/doc/function-reference/)
if ( function_exists( 'pll_the_languages' ) ) {
$languages = pll_the_languages( array( 'raw' => 1 ) );
foreach ( $languages as $lang ) {
$output .= sprintf(
'<a href="%s" class="%s">%s</a>',
esc_url( $lang['url'] ),
$lang['current_lang'] ? 'current-lang' : '',
esc_html( $lang['name'] )
);
}
}
// WPML language switcher
// (documented at wpml.org/documentation/support/wpml-coding-api/multi-language-api/)
elseif ( function_exists( 'icl_get_languages' ) ) {
$languages = icl_get_languages();
foreach ( $languages as $lang ) {
$output .= sprintf(
'<a href="%s" class="%s">%s</a>',
esc_url( $lang['url'] ),
$lang['active'] ? 'current-lang' : '',
esc_html( $lang['native_name'] )
);
}
}
$output .= '</div>';
return $output;
}
Working with translation plugins is likely to be a large part of your work, so let’s look at this aspect next.
Working with translation plugins: compatibility and optimization
Each WordPress translation plugin handles Block themes in a unique way. Understanding the approaches different solutions take helps you build compatibility and flexibility from the start.
WPML’s Full Site Editing documentation outlines how you need a specific configuration for Block themes:
// WPML FSE compatibility based on official documentation
add_action( 'init', function() {
if ( ! defined( 'WPML_VERSION' ) ) {
return;
}
// FSE themes are automatically detected in WPML 4.5.3+ // Enable FSE support
add_filter( 'wpml_is_fse_theme', '__return_true' );
// Register custom strings per WPML String Translation documentation
// (documented at wpml.org/documentation/support/wpml-coding-api/wpml-hooks-reference/)
if ( function_exists( 'icl_register_string' ) ) {
icl_register_string(
'my-block-theme',
'footer-copyright',
'© My Company.'
);
}
});
Polylang Pro has supported the Site Editor since version 3.2. The plugin handles Block themes through its standard string translation interface:
// Polylang string registration per official documentation
if ( function_exists( 'pll_register_string' ) ) {
pll_register_string(
'Footer Copyright',
'© My Company.',
'my-block-theme',
true // Multiline support
);
}
TranslatePress’ documentation shows that certain dynamic elements need exclusion for optimal performance:
// TranslatePress optimization based on official recommendations
// (documented at translatepress.com/docs/developers/)
add_filter( 'trp_stop_translating_page', function( $stop, $url ) {
// Skip admin and API requests per TranslatePress documentation
if ( is_admin() || wp_is_json_request() ) {
return true;
}
// Skip pattern preview URLs that can cause rendering issues
if ( strpos( $url, 'pattern-preview' ) !== false ) {
return true;
}
return $stop;
}, 10, 2 );
Finally, there are a few general-purpose tips to pass on when working with third-party codebases (such as plugins). First, make sure you use a systematic approach to debugging translation issues.
// Debug helper for translation issues
function my_theme_debug_translations() {
if ( ! WP_DEBUG || ! current_user_can( 'manage_options' ) ) {
return;
}
error_log( 'Text domain loaded: ' . is_textdomain_loaded(
'my-block-theme' ) );
error_log( 'Current locale: ' . get_locale() );
error_log( 'Translation test: ' . __(
'Hello World',
'my-block-theme'
)
);
// Check JSON translations for blocks
$json_file = WP_LANG_DIR . '/themes/my-block-theme-' . get_locale() . '-script-handle.json';
error_log( 'JSON translation exists: ' . file_exists( $json_file ) );
}
add_action( 'init', 'my_theme_debug_translations' );
Site caching can interfere with translation updates, so you may want to clear caches when translation files change:
# Clear WordPress transients
wp transient delete --all
# Generate fresh translation files
wp i18n make-pot . languages/my-block-theme.pot
wp i18n make-json languages/ --no-purge
Performance optimization becomes critical with translation plugins. Each plugin adds database queries and processing overhead, which again benefits from caching frequently used translations:
function my_theme_cached_translation( $text, $domain = 'my-block-theme' ) {
$cache_key = 'translation_' . md5( $text . get_locale() );
$cached = wp_cache_get( $cache_key, 'my_theme_translations' );
if ( false === $cached ) {
$cached = __( $text, $domain );
wp_cache_set( $cache_key, $cached, 'my_theme_translations', HOUR_IN_SECONDS );
}
return $cached;
}
Alternatively, it might be wise to skip caching until you’re ready to deploy. Using a staging environment is ideal for this, and you won’t typically need the performance boost that caching provides.
Summary
The Block theme internationalization requires you to work with both WordPress translation methods and utilize new approaches within the Site Editor.
By configuring your theme metadata, implementing template strategies, and understanding the requirements of translation plugins, you can create multilingual Block themes that perform well and provide a high-quality user experience.
When you’re ready to launch, Kinsta’s managed hosting for WordPress delivers the performance and global reach your site needs, with built-in caching, a 37-location CDN, and tools like Git integration and staging.