• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Step-by-Step Guide to building a custom automatic translation switcher block for Gutenberg using PHP block-render callbacks

Step-by-Step Guide to building a custom automatic translation switcher block for Gutenberg using PHP block-render callbacks

Leveraging PHP Block-Render Callbacks for a Custom Gutenberg Translation Switcher

Building a truly dynamic and context-aware translation switcher within WordPress often necessitates more than just client-side JavaScript. For scenarios requiring server-side logic, such as fetching translation data based on user roles or integrating with external translation APIs during the rendering phase, Gutenberg’s block-render callbacks offer a robust solution. This guide details the construction of a custom Gutenberg block that dynamically renders a language switcher, driven by PHP, and adaptable to various translation plugins or custom multilingual setups.

Block Registration and Server-Side Rendering Setup

The foundation of our custom block lies in its registration. We’ll define a server-side rendered block, meaning its output will be generated by a PHP callback function. This is crucial for our translation switcher, as we might need to query available languages or user preferences on the server.

First, ensure you have a WordPress plugin or theme setup where you can enqueue scripts and register blocks. For this example, we’ll assume a plugin structure. We’ll use the `register_block_type` function, specifying the `render_callback` argument.

`functions.php` or Plugin Main File

Add the following code to your plugin’s main PHP file or your theme’s `functions.php` (though a plugin is recommended for better maintainability).

This code registers our block, `my-blocks/translation-switcher`, and points to a PHP function, `render_translation_switcher_block`, that will handle its server-side rendering.

<?php
/**
 * Plugin Name: My Custom Translation Switcher Block
 * Description: Adds a custom translation switcher block for Gutenberg.
 * Version: 1.0
 * Author: Antigravity
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly.
}

/**
 * Register the custom translation switcher block.
 */
function my_blocks_register_translation_switcher() {
    register_block_type( 'my-blocks/translation-switcher', array(
        'render_callback' => 'render_translation_switcher_block',
        'attributes'      => array(
            'show_flags' => array(
                'type'    => 'boolean',
                'default' => true,
            ),
            'default_language' => array(
                'type'    => 'string',
                'default' => '',
            ),
        ),
    ) );
}
add_action( 'init', 'my_blocks_register_translation_switcher' );

/**
 * Server-side rendering callback for the translation switcher block.
 *
 * @param array $attributes The block attributes.
 * @return string HTML output for the block.
 */
function render_translation_switcher_block( $attributes ) {
    // Rendering logic will go here.
    $show_flags = isset( $attributes['show_flags'] ) ? (bool) $attributes['show_flags'] : true;
    $default_language = isset( $attributes['default_language'] ) ? sanitize_text_field( $attributes['default_language'] ) : '';

    // Placeholder for actual translation logic.
    $available_languages = get_available_translation_languages(); // This is a hypothetical function.
    $current_language = get_current_translation_language(); // This is a hypothetical function.

    if ( empty( $available_languages ) ) {
        return '<p>No translation languages configured.</p>';
    }

    ob_start();
    ?>
    <div class="wp-block-my-blocks-translation-switcher">
        <label for="translation-switcher-select"><?php esc_html_e( 'Select Language:', 'my-blocks' ); ?></label>
        <select id="translation-switcher-select" name="translation-switcher-select">
            <?php foreach ( $available_languages as $lang_code => $lang_name ) : ?>
                <option value="<?php echo esc_attr( $lang_code ); ?>"
                    <?php selected( $lang_code, $current_language ); ?>
                    <?php if ( $show_flags ) : ?>
                        data-flag="<?php echo esc_attr( get_language_flag_url( $lang_code ) ); ?>" <!-- Hypothetical flag URL -->
                    <?php endif; ?>
                >
                    <?php echo esc_html( $lang_name ); ?>
                    <?php if ( $show_flags ) : ?>
                        <img src="<?php echo esc_attr( get_language_flag_url( $lang_code ) ); ?>" alt="<?php echo esc_attr( $lang_name ); ?> flag" style="width: 20px; height: auto; margin-left: 5px;">
                    <?php endif; ?>
                </option>
            <?php endforeach; ?>
        </select>
        <!-- JavaScript will be needed to handle the actual switching -->
    </div>
    <?php
    return ob_get_clean();
}

/**
 * Hypothetical function to get available translation languages.
 * Replace with actual logic based on your translation plugin (e.g., WPML, Polylang).
 *
 * @return array An associative array of language codes and names.
 */
function get_available_translation_languages() {
    // Example for a hypothetical multilingual plugin.
    // In a real scenario, you'd query WPML, Polylang, or your custom solution.
    return array(
        'en' => 'English',
        'es' => 'Español',
        'fr' => 'Français',
    );
}

/**
 * Hypothetical function to get the current translation language.
 * Replace with actual logic.
 *
 * @return string The current language code.
 */
function get_current_translation_language() {
    // Example: Get from URL parameter, cookie, or WPML/Polylang functions.
    // For demonstration, let's assume it's 'en'.
    return 'en';
}

/**
 * Hypothetical function to get the URL for a language flag.
 * Replace with actual logic.
 *
 * @param string $lang_code The language code.
 * @return string The URL to the flag image.
 */
function get_language_flag_url( $lang_code ) {
    // Example: Assuming flags are in a theme/plugin assets folder.
    // You might need to adjust paths based on your setup.
    $flag_urls = array(
        'en' => get_template_directory_uri() . '/assets/flags/us.png', // Example path
        'es' => get_template_directory_uri() . '/assets/flags/es.png',
        'fr' => get_template_directory_uri() . '/assets/flags/fr.png',
    );
    return isset( $flag_urls[ $lang_code ] ) ? $flag_urls[ $lang_code ] : '';
}

/**
 * Enqueue editor assets for block preview and settings.
 */
function my_blocks_editor_assets() {
    wp_enqueue_script(
        'my-blocks-translation-switcher-editor-script',
        plugin_dir_url( __FILE__ ) . 'build/index.js', // Path to your compiled JS
        array( 'wp-blocks', 'wp-editor', 'wp-components', 'wp-element' ),
        filemtime( plugin_dir_path( __FILE__ ) . 'build/index.js' )
    );

    // Pass data to the editor script if needed (e.g., available languages)
    wp_localize_script( 'my-blocks-translation-switcher-editor-script', 'myBlocksTranslationSwitcher', array(
        'availableLanguages' => get_available_translation_languages(),
        'defaultLanguage'    => '', // Can be dynamic
    ) );
}
add_action( 'enqueue_block_editor_assets', 'my_blocks_editor_assets' );
?>

Gutenberg Editor Integration (JavaScript)

While the rendering is handled by PHP, the Gutenberg editor requires JavaScript to define the block’s interface, attributes, and how it appears in the editor. This involves creating a `block.json` file and a JavaScript file for the editor.

`block.json`

Create a `block.json` file in your plugin’s root directory (or a dedicated `blocks` subfolder). This file describes the block to Gutenberg.

{
    "apiVersion": 2,
    "name": "my-blocks/translation-switcher",
    "title": "Translation Switcher",
    "category": "widgets",
    "icon": "translation",
    "description": "A custom block to switch between website languages.",
    "keywords": [ "translation", "language", "switcher", "multilingual" ],
    "attributes": {
        "show_flags": {
            "type": "boolean",
            "default": true
        },
        "default_language": {
            "type": "string",
            "default": ""
        }
    },
    "editorScript": "file:./build/index.js",
    "editorStyle": "file:./build/index.css",
    "style": "file:./build/style.css",
    "render": "file:./render.php"
}

Note: The `”render”: “file:./render.php”` line is an alternative way to specify the render callback if you prefer to keep it in a separate file. However, for this example, we’ve registered it directly in PHP using `register_block_type` with the `render_callback` argument, which is often more flexible.

Editor JavaScript (`src/index.js`)

This JavaScript file will define the block’s behavior in the editor. You’ll need a build process (like `@wordpress/scripts`) to compile this into `build/index.js`.

/**
 * WordPress dependencies
 */
import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { InspectorControls } from '@wordpress/block-editor';
import { PanelBody, ToggleControl, SelectControl } from '@wordpress/components';

/**
 * Internal dependencies
 */
import './style.scss'; // For front-end styles
import './editor.scss'; // For editor-only styles

// Get available languages from localized data
const availableLanguages = window.myBlocksTranslationSwitcher.availableLanguages || {};
const languageOptions = Object.keys(availableLanguages).map(code => ({
    label: available_languages[code],
    value: code,
}));

// Add a default empty option for 'default_language'
languageOptions.unshift({ label: __('Select a default language...', 'my-blocks'), value: '' });

/**
 * Register the block
 */
registerBlockType( 'my-blocks/translation-switcher', {
    title: __( 'Translation Switcher', 'my-blocks' ),
    icon: 'translation',
    category: 'widgets',
    attributes: {
        show_flags: {
            type: 'boolean',
            default: true,
        },
        default_language: {
            type: 'string',
            default: '',
        },
    },
    edit: ( { attributes, setAttributes } ) => {
        const { show_flags, default_language } = attributes;

        const onChangeShowFlags = ( newValue ) => {
            setAttributes( { show_flags: newValue } );
        };

        const onChangeDefaultLanguage = ( newValue ) => {
            setAttributes( { default_language: newValue } );
        };

        // This is a simplified preview for the editor.
        // The actual rendering happens server-side.
        return (
            <>
                <InspectorControls>
                    <PanelBody title={ __( 'Language Settings', 'my-blocks' ) } initialOpen={ true }>
                        <ToggleControl
                            label={ __( 'Show Flags', 'my-blocks' ) }
                            checked={ show_flags }
                            onChange={ onChangeShowFlags }
                        />
                        <SelectControl
                            label={ __( 'Default Language', 'my-blocks' ) }
                            value={ default_language }
                            options={ languageOptions }
                            onChange={ onChangeDefaultLanguage }
                            help={ __( 'Select the language to be pre-selected.', 'my-blocks' ) }
                        />
                    </PanelBody>
                </InspectorControls>
                <div className="editor-preview">
                    <p>{ __( 'Translation Switcher (Editor Preview)', 'my-blocks' ) }</p>
                    <select>
                        { languageOptions.map( option => (
                            <option key={ option.value } value={ option.value }>
                                { option.label }
                            </option>
                        ) ) }
                    </select>
                    { show_flags && <span> (Flags enabled) </span> }
                </div>
            </>
        );
    },
    save: () => {
        // Return null for server-side rendered blocks.
        // The server-side callback handles the output.
        return null;
    },
} );

Editor Styles (`src/editor.scss`)

.editor-preview {
    border: 1px dashed #ccc;
    padding: 15px;
    background-color: #f9f9f9;
    text-align: center;
    select {
        margin: 10px auto;
        display: block;
    }
}

Front-end Styles (`src/style.scss`)

.wp-block-my-blocks-translation-switcher {
    display: flex;
    align-items: center;
    gap: 10px;
    label {
        font-weight: bold;
    }
    select {
        padding: 8px;
        border: 1px solid #ccc;
        border-radius: 4px;
    }
    img {
        vertical-align: middle;
        margin-left: 5px;
        border: 1px solid #eee; /* Subtle border for flags */
    }
}

Implementing the Server-Side Rendering Logic

The `render_translation_switcher_block` function in our PHP code is where the magic happens for server-side rendering. It receives the block’s attributes and must return the HTML output.

`render_translation_switcher_block` Function Breakdown

function render_translation_switcher_block( $attributes ) {
    // 1. Retrieve and sanitize attributes
    $show_flags = isset( $attributes['show_flags'] ) ? (bool) $attributes['show_flags'] : true;
    $default_language = isset( $attributes['default_language'] ) ? sanitize_text_field( $attributes['default_language'] ) : '';

    // 2. Fetch translation data (replace with your plugin's API)
    $available_languages = get_available_translation_languages(); // Hypothetical
    $current_language = get_current_translation_language(); // Hypothetical

    // 3. Handle empty state
    if ( empty( $available_languages ) ) {
        return '<p>' . esc_html__( 'No translation languages configured.', 'my-blocks' ) . '</p>';
    }

    // 4. Start output buffering
    ob_start();
    ?>
    <div class="wp-block-my-blocks-translation-switcher">
        <label for="translation-switcher-select-<?php echo esc_attr( uniqid() ); ?>"><?php esc_html_e( 'Select Language:', 'my-blocks' ); ?></label>
        <select id="translation-switcher-select-<?php echo esc_attr( uniqid() ); ?>" name="translation-switcher-select">
            <?php foreach ( $available_languages as $lang_code => $lang_name ) : ?>
                <option value="<?php echo esc_attr( $lang_code ); ?>"
                    <?php selected( $lang_code, $current_language ); ?>
                    <?php if ( $show_flags ) : ?>
                        data-flag="<?php echo esc_attr( get_language_flag_url( $lang_code ) ); ?>" <!-- Hypothetical flag URL -->
                    <?php endif; ?>
                >
                    <?php echo esc_html( $lang_name ); ?>
                    <?php if ( $show_flags && get_language_flag_url( $lang_code ) ) : ?>
                        <img src="<?php echo esc_url( get_language_flag_url( $lang_code ) ); ?>" alt="<?php echo esc_attr( $lang_name ); ?> flag" class="language-flag">
                    <?php endif; ?>
                </option>
            <?php endforeach; ?>
        </select>
        <!-- Client-side JavaScript will be required to handle the actual redirection or AJAX call -->
    </div>
    <?php
    // 5. Return buffered output
    return ob_get_clean();
}

Key Considerations for `render_translation_switcher_block`:**

  • Attribute Handling: Safely retrieve and sanitize attributes passed from the editor.
  • Translation Plugin Integration: The hypothetical functions `get_available_translation_languages()`, `get_current_translation_language()`, and `get_language_flag_url()` are placeholders. You MUST replace these with the actual functions provided by your translation plugin (e.g., WPML’s `icl_get_languages`, Polylang’s `pll_the_languages`) or your custom implementation.
  • Output Buffering: `ob_start()` and `ob_get_clean()` are used to capture the HTML output generated within the PHP block, which is then returned as a string.
  • Unique IDs: Using `uniqid()` for the `id` attribute of the label and select elements ensures uniqueness if multiple switchers are on the same page.
  • Accessibility: Ensure proper `label` and `for` attributes are used.
  • Security: Always use escaping functions like `esc_html()`, `esc_attr()`, and `esc_url()` for output.

Client-Side Interactivity (JavaScript)

The PHP rendering provides the HTML structure. However, the actual act of switching languages typically requires client-side JavaScript to either redirect the user or perform an AJAX request. This JavaScript would be enqueued on the front-end.

Front-end JavaScript (`src/frontend.js` – Example)

You would enqueue this script conditionally, perhaps only when the block is present on the page. For simplicity, let’s assume it’s enqueued globally or via `enqueue_block_assets`.

document.addEventListener( 'DOMContentLoaded', function() {
    const switcherSelects = document.querySelectorAll( '.wp-block-my-blocks-translation-switcher select[name="translation-switcher-select"]' );

    switcherSelects.forEach( selectElement => {
        selectElement.addEventListener( 'change', function( event ) {
            const selectedLang = event.target.value;
            if ( ! selectedLang ) {
                return; // No language selected or invalid.
            }

            // --- Implement your translation switching logic here ---
            // This is highly dependent on your translation plugin.

            // Example 1: Simple redirection (common for Polylang, WPML)
            // You might need to construct the URL based on the current page's slug and the selected language.
            // This often requires server-side logic or a JS library that knows how to build these URLs.
            // For a basic example, let's assume a simple URL structure:
            // const baseUrl = window.location.origin;
            // const currentPath = window.location.pathname.split('/').filter(Boolean).slice(1).join('/'); // Get path excluding the first segment (often the current language)
            // const newUrl = `${baseUrl}/${selectedLang}/${currentPath}`; // This is a simplification!
            // window.location.href = newUrl;

            // Example 2: Using WPML's JavaScript API (if available and configured)
            // if ( typeof wpml_ls_redirect !== 'undefined' ) {
            //     wpml_ls_redirect( selectedLang );
            // }

            // Example 3: Using Polylang's JavaScript API (if available)
            // if ( typeof PLL !== 'undefined' && PLL.hasOwnProperty('க்கவும்') ) {
            //     const currentUrl = window.location.href;
            //     const newUrl = PLL.க்கவும்(currentUrl, selectedLang); // This is a simplified representation of Polylang's URL switching function
            //     window.location.href = newUrl;
            // }

            // Example 4: AJAX request to a custom endpoint
            // fetch( '/wp-json/my-blocks/v1/switch-language', {
            //     method: 'POST',
            //     headers: { 'Content-Type': 'application/json' },
            //     body: JSON.stringify({ lang: selectedLang, current_url: window.location.href })
            // })
            // .then( response => response.json() )
            // .then( data => {
            //     if ( data.success && data.data.new_url ) {
            //         window.location.href = data.data.new_url;
            //     } else {
            //         console.error( 'Language switch failed:', data.data.message );
            //     }
            // })
            // .catch( error => console.error( 'Error:', error ) );

            console.warn( 'Translation switching logic not fully implemented. Implement redirection or AJAX based on your plugin.' );
            alert( `Selected language: ${selectedLang}. Implement actual switching logic.` );
        } );
    } );
} );

To use this, you’d enqueue it using `wp_enqueue_script` in your plugin’s main file, ensuring it loads on the front-end.

/**
 * Enqueue front-end scripts.
 */
function my_blocks_frontend_scripts() {
    // Only enqueue if the block is likely to be present, or enqueue always.
    // A more advanced approach would check for the block's presence.
    wp_enqueue_script(
        'my-blocks-translation-switcher-frontend',
        plugin_dir_url( __FILE__ ) . 'build/frontend.js', // Path to your compiled JS
        array( 'jquery' ), // Or other dependencies
        filemtime( plugin_dir_path( __FILE__ ) . 'build/frontend.js' ),
        true // Load in footer
    );
}
add_action( 'wp_enqueue_scripts', 'my_blocks_frontend_scripts' );

Advanced Considerations and Best Practices

  • Translation Plugin Compatibility: The most critical aspect is integrating with your specific translation plugin. Each plugin (WPML, Polylang, TranslatePress, etc.) has its own API for retrieving languages, setting the current language, and generating translated URLs. Thoroughly consult your plugin’s documentation.
  • Performance: For very large sites or complex translation setups, ensure your `get_available_translation_languages()` and related functions are performant. Consider caching translation data if necessary.
  • SEO: Ensure your language switching mechanism is SEO-friendly. This usually involves `hreflang` tags, which are typically handled by the translation plugins themselves. The switcher should correctly link to translated versions of pages.
  • User Experience: Provide clear visual cues (flags, language names) and ensure the switching mechanism is intuitive. The `default_language` attribute can pre-select a user’s preferred language if known.
  • AJAX vs. Redirection: Decide whether language switching should redirect the entire page or use AJAX to update content dynamically. Redirection is simpler and generally better for SEO. AJAX can offer a smoother user experience but requires more complex implementation and potentially custom REST API endpoints.
  • REST API Integration: If you opt for an AJAX-based switcher, you’ll likely need to register a custom REST API endpoint in WordPress to handle the language switching request and return the appropriate translated URL or content.
  • Block Editor Preview: The `edit` function in `src/index.js` provides a basic preview. For more accurate previews, especially if server-side logic heavily influences the output, you might explore `useSelect` and `useEntityProp` hooks to fetch relevant data or even use `ServerSideRender` component within the editor, though this adds complexity.

By combining PHP’s server-side capabilities with Gutenberg’s block architecture and front-end JavaScript, you can create a powerful and flexible translation switcher that integrates seamlessly into your WordPress site.

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • How to build custom FSE Block Themes extensions utilizing modern Filesystem API schemas
  • Step-by-Step Guide to building a custom CSV bulk exporter block for Gutenberg using React components
  • Optimizing WooCommerce cart response times by lazy loading custom vendor commission records assets
  • Troubleshooting SQL query deadlocks in production when using modern Genesis child themes wrappers
  • How to analyze and reduce CPU consumption of custom Observer Pattern event mediators

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (648)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (858)
  • PHP (5)
  • PHP Development (38)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (627)
  • SEO & Growth (492)
  • Server (23)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (297)
  • WordPress Theme Development (357)

Recent Posts

  • How to build custom FSE Block Themes extensions utilizing modern Filesystem API schemas
  • Step-by-Step Guide to building a custom CSV bulk exporter block for Gutenberg using React components
  • Optimizing WooCommerce cart response times by lazy loading custom vendor commission records assets

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (858)
  • Debugging & Troubleshooting (648)
  • Security & Compliance (627)
  • SEO & Growth (492)
  • Business & Monetization (390)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala