• 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 Alpine.js lightweight states

Step-by-Step Guide to building a custom automatic translation switcher block for Gutenberg using Alpine.js lightweight states

Leveraging Alpine.js for a Lightweight Gutenberg Translation Switcher

For e-commerce platforms built on WordPress, offering a seamless multilingual experience is paramount. While robust translation plugins exist, sometimes a custom, lightweight solution is desired for specific components, like a header or footer translation switcher. This guide details building a custom Gutenberg block that integrates Alpine.js for dynamic state management, enabling a responsive and efficient translation switcher without heavy JavaScript dependencies.

Prerequisites and Project Setup

Before diving into the code, ensure you have a local WordPress development environment set up. We’ll be using the WordPress `@wordpress/scripts` package for building our block assets. Install it in your theme’s or plugin’s JavaScript directory:

  • Navigate to your theme’s or plugin’s root directory in your terminal.
  • Run the following command to install the necessary build tools:
npm install --save-dev @wordpress/scripts

Next, configure your `package.json` to include build scripts. Add these lines to your `package.json` file:

{
  "scripts": {
    "build": "wp-scripts build",
    "start": "wp-scripts start"
  }
}

This setup allows you to run `npm run build` to compile your JavaScript and CSS, and `npm run start` for continuous development with live reloading.

Gutenberg Block Registration and Structure

We’ll create a custom plugin or theme component to house our Gutenberg block. The core of our block will be defined in PHP, registering it with WordPress. The JavaScript will handle the editor interface and the frontend interactivity.

Create a PHP file (e.g., `translation-switcher-block.php`) in your `wp-content/plugins/` directory. Register the block using `register_block_type`:

<?php
/**
 * Plugin Name: Custom Translation Switcher Block
 * Description: A custom Gutenberg block for a translation switcher using Alpine.js.
 * Version: 1.0.0
 * Author: Antigravity
 */

function custom_translation_switcher_block_register() {
    register_block_type( __DIR__ . '/build' );
}
add_action( 'init', 'custom_translation_switcher_block_register' );
?>

Now, create a `src` directory within your plugin’s root. Inside `src`, create `index.js` and `editor.scss` (for the editor styles) and `style.scss` (for frontend styles). Also, create a `block.json` file to define the block’s metadata.

{
  "apiVersion": 2,
  "name": "custom/translation-switcher",
  "version": "0.1.0",
  "title": "Translation Switcher",
  "category": "widgets",
  "icon": "translation",
  "description": "A custom translation switcher block.",
  "attributes": {
    "defaultLanguage": {
      "type": "string",
      "default": "en"
    },
    "availableLanguages": {
      "type": "array",
      "default": [
        {"code": "en", "name": "English"},
        {"code": "es", "name": "Español"},
        {"code": "fr", "name": "Français"}
      ]
    }
  },
  "editorScript": "file:./build/index.js",
  "editorStyle": "file:./build/index.css",
  "style": "file:./build/style.css"
}

The `block.json` defines our block’s name, title, category, and importantly, its attributes. We’ve added `defaultLanguage` and `availableLanguages` to configure the switcher. The `editorScript` and `style` properties point to the compiled assets.

Frontend JavaScript with Alpine.js

This is where Alpine.js shines. We’ll enqueue Alpine.js and then write our block’s frontend JavaScript. The core idea is to have a dropdown or list of languages, and when a user selects one, we’ll trigger a redirect or update content (depending on your translation strategy).

First, enqueue Alpine.js in your PHP file. A good place is within the `init` action or a dedicated script enqueue function:

<?php
function custom_translation_switcher_enqueue_scripts() {
    // Enqueue Alpine.js
    wp_enqueue_script(
        'alpinejs',
        'https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js', // Use a specific version or host locally
        array(),
        '3.x.x',
        true // Load in footer
    );

    // Enqueue block script (this will be compiled by @wordpress/scripts)
    wp_enqueue_script(
        'custom-translation-switcher-block-frontend',
        plugins_url( 'build/frontend.js', __FILE__ ),
        array( 'alpinejs' ), // Dependency on Alpine.js
        filemtime( plugin_dir_path( __FILE__ ) . 'build/frontend.js' )
    );
}
add_action( 'wp_enqueue_scripts', 'custom_translation_switcher_enqueue_scripts' );
?>

Now, create `src/frontend.js`. This file will contain the logic for our Alpine.js component. We’ll use a simple dropdown for demonstration.

document.addEventListener('DOMContentLoaded', () => {
    // Find all instances of our translation switcher block
    const switcherElements = document.querySelectorAll('[data-translation-switcher]');

    switcherElements.forEach(element => {
        const defaultLang = element.dataset.defaultLanguage || 'en';
        const availableLangs = JSON.parse(element.dataset.availableLanguages || '[]');

        // Initialize Alpine.js component
        Alpine.data('translationSwitcher', () => ({
            open: false,
            currentLang: defaultLang,
            availableLanguages: availableLangs,
            init() {
                // Set initial current language if not default
                const currentUrlLang = this.detectLangFromUrl();
                if (currentUrlLang && this.availableLanguages.some(lang => lang.code === currentUrlLang)) {
                    this.currentLang = currentUrlLang;
                }
            },
            toggle() {
                this.open = !this.open;
            },
            selectLanguage(langCode) {
                this.currentLang = langCode;
                this.open = false;
                this.redirectToLanguage(langCode);
            },
            detectLangFromUrl() {
                // Simple URL detection: e.g., example.com/es/page
                const pathSegments = window.location.pathname.split('/').filter(segment => segment !== '');
                if (pathSegments.length > 0 && this.availableLanguages.some(lang => lang.code === pathSegments[0])) {
                    return pathSegments[0];
                }
                return null;
            },
            redirectToLanguage(langCode) {
                if (langCode === defaultLang) {
                    // Redirect to base URL if switching back to default
                    window.location.href = window.location.origin + window.location.pathname.replace(/^\/[a-z]{2}\//, '/');
                } else {
                    // Prepend language code to the current path
                    const currentPath = window.location.pathname;
                    const newPath = `/${langCode}${currentPath}`;
                    window.location.href = window.location.origin + newPath;
                }
            }
        }));

        // Manually bootstrap the Alpine component if not automatically detected
        // This might be necessary if the block is rendered dynamically
        Alpine.initTree(element);
    });
});

In `src/index.js`, we’ll define the editor-side of the block. This file will use the WordPress Block Editor API.

import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl, SelectControl } from '@wordpress/components';
import { Fragment } from '@wordpress/element';
import './editor.scss';
import './style.scss';

const { attributes, setAttributes } = wp.data.select('core/block-editor');

registerBlockType('custom/translation-switcher', {
    edit: ({ attributes, setAttributes }) => {
        const blockProps = useBlockProps();
        const { defaultLanguage, availableLanguages } = attributes;

        const languageOptions = availableLanguages.map(lang => ({
            label: lang.name,
            value: lang.code,
        }));

        const handleLanguageChange = (newLang) => {
            setAttributes({ defaultLanguage: newLang });
        };

        const handleAddLanguage = () => {
            const newLang = { code: `lang${availableLanguages.length + 1}`, name: `Language ${availableLanguages.length + 1}` };
            setAttributes({ availableLanguages: [...availableLanguages, newLang] });
        };

        const handleUpdateLanguage = (index, field, value) => {
            const updatedLangs = [...availableLanguages];
            updatedLangs[index][field] = value;
            setAttributes({ availableLanguages: updatedLangs });
        };

        const handleRemoveLanguage = (index) => {
            const updatedLangs = availableLanguages.filter((_, i) => i !== index);
            setAttributes({ availableLanguages: updatedLangs });
        };

        return (
            <>
                <InspectorControls>
                    <PanelBody title="Translation Settings" initialOpen={ true }>
                        <TextControl
                            label="Default Language Code"
                            value={ defaultLanguage }
                            onChange={ handleLanguageChange }
                        />
                        <h3>Available Languages</h3>
                        { availableLanguages.map((lang, index) => (
                            <div key={ index } style={ { marginBottom: '10px', border: '1px solid #ccc', padding: '10px' } }>
                                <TextControl
                                    label={ `Language ${index + 1} Name` }
                                    value={ lang.name }
                                    onChange={ (value) => handleUpdateLanguage(index, 'name', value) }
                                />
                                <TextControl
                                    label={ `Language ${index + 1} Code` }
                                    value={ lang.code }
                                    onChange={ (value) => handleUpdateLanguage(index, 'code', value) }
                                />
                                { availableLanguages.length > 1 && (
                                    <button onClick={ () => handleRemoveLanguage(index) }>Remove</button>
                                ) }
                            </div>
                        )) }
                        <button onClick={ handleAddLanguage }>Add Language</button>
                    </PanelBody>
                </InspectorControls>
                <div { ...blockProps } data-translation-switcher="" data-default-language={ defaultLanguage } data-available-languages={ JSON.stringify(availableLanguages) }>
                    <div x-data="translationSwitcher()">
                        <button @click="toggle()">
                            { defaultLanguage }
                        </button>
                        <ul x-show="open" @click.away="open = false">
                            { availableLanguages.map(lang => (
                                <li key={ lang.code } @click="selectLanguage('{ lang.code }')">
                                    { lang.name }
                                </li>
                            )) }
                        </ul>
                    </div>
                </div>
            </>
        );
    },
    save: ({ attributes }) => {
        const { defaultLanguage, availableLanguages } = attributes;
        // The frontend script will handle the Alpine.js logic.
        // We just need to output the data attributes.
        return (
            <div
                data-translation-switcher=""
                data-default-language={ defaultLanguage }
                data-available-languages={ JSON.stringify(availableLanguages) }
            />
        );
    },
});

In the `edit` function, we use `InspectorControls` to provide settings for the default language and available languages. The `useBlockProps` hook applies necessary classes and attributes. The `save` function is crucial: it outputs only the necessary `data-*` attributes. The actual interactive UI is rendered by Alpine.js on the frontend, triggered by the presence of these attributes.

Styling the Translation Switcher

Create `src/editor.scss` and `src/style.scss` for your block’s styles. Here’s a basic example:

/* src/editor.scss */
.wp-block-custom-translation-switcher {
    border: 1px dashed #ccc;
    padding: 10px;
    text-align: center;
}

/* src/style.scss */
.wp-block-custom-translation-switcher {
    display: inline-block; /* Or block, depending on desired layout */
    position: relative;
    font-family: sans-serif;

    button {
        background-color: #f0f0f0;
        border: 1px solid #ddd;
        padding: 8px 12px;
        cursor: pointer;
        border-radius: 4px;
        display: flex;
        align-items: center;
        gap: 5px;

        &:hover {
            background-color: #e0e0e0;
        }
    }

    ul {
        position: absolute;
        top: 100%;
        left: 0;
        background-color: #fff;
        border: 1px solid #ddd;
        list-style: none;
        padding: 0;
        margin: 5px 0 0 0;
        min-width: 100%;
        box-shadow: 0 2px 5px rgba(0,0,0,0.1);
        z-index: 10;
        border-radius: 4px;

        li {
            padding: 8px 12px;
            cursor: pointer;

            &:hover {
                background-color: #f0f0f0;
            }
        }
    }
}

Building and Activation

After setting up your files, run the build command in your terminal from the plugin’s root directory:

npm run build

This will compile your `src/index.js`, `src/editor.scss`, and `src/style.scss` into the `build/` directory. Activate the “Custom Translation Switcher Block” plugin in your WordPress admin area. You can now add the “Translation Switcher” block to your posts or pages via the Gutenberg editor.

Advanced Considerations and Customization

Translation Strategy: The provided `redirectToLanguage` function assumes a URL-based translation strategy (e.g., `example.com/es/page`). For other strategies like cookie-based or API-driven content switching, you’ll need to modify the `redirectToLanguage` function and potentially the frontend JavaScript to interact with your translation plugin’s API or store language preferences in cookies.

SEO: For URL-based translation, ensure your SEO strategy accounts for alternate language versions using `hreflang` tags. This block doesn’t handle `hreflang` generation; that typically falls to your main translation plugin or custom theme logic.

Accessibility: Enhance the dropdown with ARIA attributes for better screen reader support. The current implementation is basic; consider adding `aria-haspopup`, `aria-expanded`, and managing focus appropriately.

Hosting Alpine.js: For production, consider hosting Alpine.js locally within your plugin or theme to reduce external dependencies and improve load times. You can download the CDN version and enqueue it from your own server.

Dynamic Content Loading: If your translation strategy involves loading content dynamically via AJAX without page reloads, you’ll need to adapt the Alpine.js component to fetch and update content based on the selected language, rather than redirecting.

By integrating Alpine.js with a custom Gutenberg block, you gain fine-grained control over your translation switcher’s appearance and behavior, offering a lightweight yet powerful solution for multilingual e-commerce sites.

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 analyze and reduce CPU consumption of custom Action-hook Event Mediator event mediators
  • Designing audit logs for enterprise WordPress setups tracking internal user modifications to shipping tracking histories
  • Debugging and Resolving deep-seated hook priority conflicts in third-party Zapier dynamic webhooks connectors
  • Step-by-Step Guide: Offloading high-frequency shipping tracking histories metadata writes to a Redis KV store
  • How to implement custom REST API Controllers endpoints with token authentication in Gutenberg blocks

Categories

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

Recent Posts

  • How to analyze and reduce CPU consumption of custom Action-hook Event Mediator event mediators
  • Designing audit logs for enterprise WordPress setups tracking internal user modifications to shipping tracking histories
  • Debugging and Resolving deep-seated hook priority conflicts in third-party Zapier dynamic webhooks connectors

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (872)
  • Debugging & Troubleshooting (658)
  • Security & Compliance (639)
  • 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