This guide provides comprehensive instructions for implementing internationalization in WordPress blocks.
Table of Contents
- Overview
- PHP Internationalization
- JavaScript Internationalization
- Multiple Block Registration
- Translation File Generation
- Translation Workflow
- Testing Translations
- Best Practices
Overview
Internationalization (i18n) is the process of preparing your code to support multiple languages. WordPress provides robust i18n functions for both PHP and JavaScript that allow translators to create localized versions of your blocks.
Key Principles
- Text Domain: Use a consistent text domain throughout your block
- Context: Provide meaningful context for translators
- Escaping: Always use proper escaping functions for security
- Consistency: Maintain consistent naming conventions
PHP Internationalization
Basic Functions
Use WordPress i18n functions with proper escaping:
// Basic translation with context
<?php echo esc_html_x( 'String to translate', 'Block <block name> - usage description where string is used', 'text-domain' ); ?>
Context Examples
Provide clear context to help translators understand where and how the text is used:
'Block card - component label''Block card - screen reader text''Block card - select dropdown label''Block card - button label''Block card - rich text placeholder''Block card - error message''Block card - help text'
Example Implementation
// In your block's render.php or render callback function(s)
<div class="card-component">
<h3><?php echo esc_html_x( 'Featured Content', 'Block card - section heading', 'your-text-domain' ); ?></h3>
<button aria-label="<?php echo esc_attr_x( 'Read more about this content', 'Block card - button aria label', 'your-text-domain' ); ?>">
<?php echo esc_html_x( 'Read More', 'Block card - button text', 'your-text-domain' ); ?>
</button>
</div>
JavaScript Internationalization
Setup
Import the i18n functions from WordPress:
import { _x, sprintf, _n } from "@wordpress/i18n";
Usage in Components
// Basic translation
<RangeControl
label={_x("Columns", "Block cards - column count control", "your-text-domain")}
help={_x("Number of columns to display", "Block cards - column control help", "your-text-domain")}
/>
// With variables
<p>{sprintf(_x("Showing %d of %d items", "Block cards - item count display", "your-text-domain"), visible, total)}</p>
// Plural forms
<span>{sprintf(_n("%d card", "%d cards", count, "your-text-domain"), count)}</span>
Rich Text Placeholders
<RichText
placeholder={_x("Enter card title...", "Block card - title placeholder", "your-text-domain")}
value={title}
onChange={setTitle}
/>
Multiple Block Registration
When registering multiple related blocks (e.g., parent and child blocks), ensure each has proper translation support:
/**
* Sets up translations for multiple block editor scripts.
* This function should be called during the init action.
*/
function setup_block_translations() {
// Register translations for the parent 'cards' block
wp_set_script_translations(
'your-cards-editor-script', // Script handle
'your-text-domain', // Text domain
plugin_dir_path( __FILE__ ) . 'languages' // Path to translation files
);
// Register translations for the child 'card' block
wp_set_script_translations(
'your-card-editor-script', // Script handle
'your-text-domain', // Same text domain
plugin_dir_path( __FILE__ ) . 'languages' // Same path
);
}
add_action( 'init', 'setup_block_translations' );
Important Notes
- Use the same text domain for related blocks
- Ensure script handles match those registered in
block.jsonorwp_register_script() - The translation path should point to your
languagesdirectory - For parent + child blocks sharing one textdomain:
wp i18n make-jsongenerates one JSON per.jssource file. Rename each to its respective handle (e.g.,prompt-this-fr_CA-zp-prompt-this-editor-script.jsonandprompt-this-fr_CA-zp-prompt-this-item-editor-script.json)
Translation File Generation
Package.json Scripts
Add these scripts to your package.json:
{
"scripts": {
"make-pot": "wp i18n make-pot . languages/your-block-name.pot --slug=your-block-name --domain=your-text-domain --exclude=node_modules,src,vendor",
"make-json": "wp i18n make-json languages/ --no-purge",
"update-po": "wp i18n update-po languages/your-block-name.pot languages/"
}
}
Script Parameters Explained
--slug: The block/plugin slug--domain: Your text domain--exclude: Directories to exclude from scanning--no-purge: Keeps existing JSON files when generating new ones
Translation Workflow
Initial Setup
- Install Dependencies
npm install - Generate POT File
npm run make-pot- Creates
languages/directory - Generates
your-block-name.potfile with all translatable strings
- Creates
- Create Translation with Poedit
- Open Poedit
- File → New from POT/PO file
- Select your
.potfile - Choose target language (e.g., French (Canada))
- Translate strings
- Save (creates
.poand.mofiles)
Updating Existing Translations
- Update POT File
npm run make-pot - Update PO Files
npm run update-poOr in Poedit: Catalog → Update from POT file - Recompile .mo File
- Poedit: Save → automatically generates
.mo msgfmt fr_CA.po -o your-domain-fr_CA.mo(ifgettextis installed)- Fallback: WordPress POMO library (
PO::import_from_file()→MO::export_to_file())
- Poedit: Save → automatically generates
- Generate JSON Files
npm run make-json - Rename JSON Files
- Generated file:
your-block-name-fr_CA-{hash}.json - Rename to:
your-block-name-fr_CA-your-script-handle.json - Example:
cards-fr_CA-zp4-cards-editor-script.json
- Generated file:
File Structure
your-block/
├── languages/
│ ├── your-block-name.pot
│ ├── fr_CA.po
│ ├── your-text-domain-fr_CA.mo
│ └── your-block-name-fr_CA-your-script-handle.json
├── src/
├── build/
└── package.json
Block.json Metadata vs JavaScript Translations
WordPress uses two separate translation mechanisms for blocks. Understanding the difference is critical:
JavaScript Strings (edit.js)
Strings in edit.js using _x(), __(), or _n() are handled by the JSON translation file:
- Extracted by
wp i18n make-potscanning the compiledbuild/output - Converted to JSON by
wp i18n make-json - Loaded via
wp_set_script_translations( $handle, $domain, $path ) - File:
{domain}-{locale}-{script-handle}.json
Block.json Metadata (title, description, keywords)
These fields in block.json are not handled by the JSON file. WordPress automatically wraps them in _x() calls during register_block_type() and injects them as an inline script:
// What WordPress does internally when register_block_type() processes a block.json:
$title = _x( 'Summary', 'block title', 'summary' );
$description = _x( 'A semantic article summary block...', 'block description', 'summary' );
$keywords = array( _x( 'summary', 'block keyword', 'summary' ) );
This requires the .mo file to be loaded before register_block_type() runs. The .mo file must follow the naming convention {text-domain}-{locale}.mo (e.g., summary-fr_CA.mo).
Common Pitfall: .mo File Not Found
If block.json metadata fields appear untranslated in the editor, check:
- The
.mofile is named{text-domain}-{locale}.mo(not just{locale}.molikefr_CA.mo) - The textdomain (
.mofile) is loaded beforeregister_block_type()runs on theinithook - The
textdomainfield inblock.jsonmatches the loaded textdomain
Testing Translations
WordPress Admin Testing
- Switch Language
- Go to Settings → General
- Change Site Language to your test language
- Save changes
- Test Block Editor
- Create/edit a page
- Add your block
- Verify all UI elements are translated
- Check tooltips, placeholders, and error messages
- Test Frontend
- Visit the page on frontend
- Verify PHP translations are working
- Check accessibility labels
Development Testing
// Temporarily switch locale for testing
function test_translation() {
switch_to_locale('fr_CA');
// Your test code here
restore_previous_locale();
}
Best Practices
String Guidelines
- Be descriptive: Use clear, concise strings that provide context
- Avoid concatenation: Don’t split sentences across multiple translation calls
- Use placeholders: For dynamic content, use
sprintf()with placeholders - Consider length: Account for text expansion in different languages
Context Best Practices
// Good - Clear context
_x('Save', 'Block card - save button', 'text-domain')
// Better - More specific context
_x('Save', 'Block card - save card settings button', 'text-domain')
Performance Considerations
- Load translations only when needed
- Use
wp_set_script_translations()after script registration - Consider lazy loading for large translation files
Accessibility
- Translate
aria-labelandaria-describedbyattributes - Provide translated screen reader text
- Ensure translated text maintains semantic meaning
Version Control
- Include
.potfiles in version control - Include
.pofiles for supported languages - Consider excluding
.mofiles (can be generated from.po) - Include properly named
.jsonfiles
Common Pitfalls
- Inconsistent text domains: Use the same domain across all files
- Missing script translations: Remember
wp_set_script_translations() - Incorrect JSON naming: Ensure JSON files match script handles
- Hard-coded strings: Always use translation functions
- Missing context: Provide meaningful context for translators
Troubleshooting
Translations Not Loading
- Check script handle matches in
wp_set_script_translations() - Verify JSON file naming convention
- Ensure text domain consistency
- Confirm
languages/directory path is correct
Missing Strings in POT
- Verify text domain in translation functions
- Check file extensions in make-pot exclude list
- Ensure functions are properly formatted
Frontend vs. Editor Issues
- PHP translations: Check
.mofile loading - JavaScript translations: Verify JSON file naming and script registration
This guide should be updated as WordPress i18n best practices evolve. Always refer to the WordPress Developer Handbook for the latest information.
