Los temas de bloques hacen que WordPress se traduzca de forma diferente a como se hace normalmente. Los archivos de plantilla PHP tradicionales con funciones de traducción no son compatibles con las plantillas HTML, bloques basados en JavaScript y el Editor de Sitios. Este cambio requiere que comprendas los sistemas de internacionalización de bloques de WordPress de forma diferente.
Esta guía proporciona estrategias para que tus temas de bloques sean multilingües. Aprenderás a superar los retos que plantea la traducción de temas de bloques, a implementar soluciones y a integrarlos con plugins de traducción.
Por qué los temas de bloques no son compatibles con los métodos tradicionales de traducción (y cómo resolverlo)
Los temas de bloques sustituyen muchos de los archivos PHP de WordPress por plantillas HTML que contienen marcado de bloques. Sin embargo, este cambio crea desafíos porque las plantillas HTML no pueden ejecutar funciones de traducción de PHP como _()
o _e()
. Como resultado, las cadenas de traducción que ya tienes quedan inutilizadas en archivos estáticos.
WordPress 6.8 aporta algunas mejoras que simplifican la internacionalización de temas de bloques. En primer lugar, los temas con encabezados Text Domain y Domain Path adecuados ya no necesitan llamadas manuales manual load_theme_textdomain()
.
En su lugar, WordPress carga automáticamente los archivos de traducción y da prioridad a wp-content/languages/themes/
sobre los directorios de temas por motivos de rendimiento.
Para empezar, configura tu tema siguiendo un enfoque clásico, añadiendo metadatos al archivo style.css
.
/*
Theme Name: My Block Theme
Text Domain: my-block-theme
Domain Path: /languages
*/
Ten en cuenta que la cabecera Text Domain debe coincidir con el nombre de la carpeta de tu tema (normalmente en kebab-case) para garantizar que los archivos de traducción se carguen automáticamente de forma correcta en las versiones recientes de WordPress.
Al igual que style.css
, tu archivo functions.php
requiere una configuración mínima:
<?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' );
La diferencia clave entre los temas clásicos y los temas de bloques es que estos últimos dividen la responsabilidad de la traducción entre el PHP del servidor y el JavaScript del cliente. Por el contrario, los temas clásicos dependen del PHP para gestionar la mayoría de las traducciones.
Cómo crear traducciones en block.json
El archivo block.json es tu «centro de configuración» para el Bloque que deseas traducir. Configurar correctamente la internacionalización garantiza que tus bloques se traduzcan correctamente tanto en el editor como en el front end.
La forma canónica de registrar un Bloque es a través de block.json
. Empezar con la configuración textdomain
significa que WordPress puede traducir los campos título, descripción y palabras clave cuando se establece textdomain
:
{
"$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"
}
}
}
Sin embargo, los escenarios que requieren «contexto» necesitan un registro del lado del servidor. El contexto, en este caso, importa porque la misma palabra puede traducirse de forma diferente en función de su uso. Por ejemplo, «post» como sustantivo frente a «post» como verbo requiere traducciones diferentes en muchos idiomas:
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' );
Las variaciones de bloque que incluyas también necesitan una denominación estructurada, porque WordPress busca patrones específicos cuando carga tus traducciones. Cada nombre de variación se convierte en parte de la clave de traducción:
{
"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"
}
}
]
}
La internacionalización de JavaScript requiere que importes las funciones i18n de WordPress y configures las traducciones de los scripts. Esto se debe a que el Editor de Sitios se ejecuta en el navegador y no en el servidor. Como las funciones de traducción de PHP no existen en JavaScript, WordPress proporciona funciones equivalentes a través del paquete @wordpress/i18n
:
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>
);
}
});
Además, es una buena idea generar archivos de traducción JSON para JavaScript porque WordPress utiliza un formato diferente para las traducciones del lado del cliente. PHP utiliza archivos .mo
, pero JavaScript necesita archivos .json
con convenciones de nomenclatura específicas. Puedes automatizar esto utilizando comandos WP-CLI:
# 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
Los archivos JSON resultantes siguen un patrón coherente: {textdomain}-{locale}-{handle}.json
. WordPress puede entonces cargarlos cuando llames a wp_set_script_translations()
.
Convertir tus plantillas HTML estáticas en componentes PHP listos para la traducción
Dado que las plantillas HTML son estáticas, trabajar con ellas para la internacionalización de temas de bloques es todo un reto, ya que las funciones y técnicas de traducción existentes no funcionarán.
Las partes de plantillas basadas en PHP podrían resolver este problema, ya que WordPress las procesa como archivos PHP a pesar de que se hacen referencia a ellas en plantillas HTML. Este enfoque híbrido mantiene la estructura del tema de Bloques al tiempo que permite contenido dinámico:
<!-- 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"} /-->
Ten en cuenta que la parte de plantilla puede contener 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 -->
Los bloques complejos necesitan el archivo render.php
porque algunos contenidos requieren un procesamiento del lado del servidor que los atributos de bloque por sí solos no pueden manejar. Las consultas a la base de datos, la lógica condicional y la generación de contenido dinámico requieren la ejecución de PHP:
// 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>
Esto significa configurar tu Bloque para que utilice el archivo de renderizado en block.json
:
{
"name": "my-theme/recent-posts",
"render": "file:./render.php",
"attributes": {
"count": {
"type": "number",
"default": 5
}
}
}
Cómo implementar la traducción de contenido dinámico para campos personalizados y entradas de usuario
Aunque el contenido dinámico es muy habitual en las webs con WordPress, puede causar problemas al traducirlo, puesto que se encuentra en la base de datos en lugar de en los archivos del tema. Por ello, cualquier plugin de traducción de terceros que utilices debe identificar y gestionar este contenido por separado de las cadenas estáticas del tema.
Aquí es donde resulta útil registrar campos personalizados con la configuración meta adecuada, ya que los plugins de traducción se conectan al sistema meta de WordPress para detectar cualquier contenido traducible. El parámetro show_in_rest
permite la compatibilidad con el Editor de Sitios:
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>';
}
Las consultas a la base de datos también necesitan un filtrado por idioma porque WordPress no filtra automáticamente el contenido por idioma. Los plugins de traducción añaden modificaciones a las consultas que debes tener en cuenta:
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 );
}
El procesamiento de formularios mezcla contenido dinámico y estático, pero las etiquetas de los formularios, los mensajes de error y las notificaciones administrativas deben traducirse teniendo en cuenta el idioma. Los destinatarios del correo electrónico también pueden variar según el idioma:
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' );
También es importante evaluar el conocimiento de idiomas de tu navegación, porque los elementos del menú, las URLs y la estructura pueden diferir entre idiomas. Es probable que tu plugin de traducción tenga una API para crear selectores de idioma:
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;
}
Es probable que trabajar con plugins de traducción ocupe una gran parte de tu trabajo, así que vamos a ver este aspecto a continuación.
Trabajar con plugins de traducción: compatibilidad y optimización
Cada plugin de traducción de WordPress gestiona los temas de bloques de una forma única. Comprender los enfoques que adoptan las distintas soluciones te ayuda a crear compatibilidad y flexibilidad desde el principio.
La documentación de WPML sobre la Edición Completa del Sitio describe cómo se necesita una configuración específica para los temas de bloques:
// 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 es compatible con el Editor de Sitios desde la versión 3.2. El plugin gestiona los temas de bloques a través de su interfaz estándar de traducción de cadenas:
// 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
);
}
La documentación de TranslatePress muestra que ciertos elementos dinámicos necesitan ser excluidos para un rendimiento óptimo:
// 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 );
Por último, hay algunos consejos generales que conviene tener en cuenta al trabajar con código de terceros (como plugins). En primer lugar, asegúrate de utilizar un enfoque sistemático para depurar los problemas de traducción.
// 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' );
El almacenamiento en caché del sitio puede interferir con las actualizaciones de traducción, por lo que te conviene borrar los cachés cuando cambien los archivos de traducción:
# 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
La optimización del rendimiento se vuelve crítica con los plugins de traducción. Cada plugin añade consultas a la base de datos y sobrecarga de procesamiento, lo que de nuevo se beneficia del almacenamiento en caché de las traducciones utilizadas con frecuencia:
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;
}
Como alternativa, puede resultar conveniente omitir el caché hasta que esté listo para el Despliegue. Para ello, lo ideal es utilizar un entorno staging, en el que normalmente no será necesario el aumento de rendimiento que proporciona el caché.
Resumen
La internacionalización de temas de bloques requiere que trabajes con los métodos de traducción de WordPress y utilices nuevos enfoques dentro del Editor del Sitio.
Al configurar los metadatos de tu tema, implementar estrategias de plantillas y comprender los requisitos de los plugin de traducción, podrás crear temas de bloques multilingües que funcionen bien y ofrezcan una experiencia de usuario de alta calidad.
Cuando estés listo para lanzar tu sitio web, el alojamiento administrado para WordPress de Kinsta te ofrece el rendimiento y el alcance global que tu sitio necesita, con caché integrado, una red de distribución de contenido (CDN) con 37 ubicaciones y herramientas como la integración con Git y staging.