Questo articolo è stato pubblicato per la prima volta nel 2010 e completamente riscritto a ottobre 2026: i commenti più vecchi si riferiscono alla versione originale.
Nel marzo del 2010 dovevo tradurre i messaggi di errore della validazione di un modulo e, con l’orgoglio di chi ha appena scoperto di poter risolvere tutto da solo, mi ero costruito un piccolo sistema casalingo: un oggetto Localization con le stringhe, una funzione _e() che le cercava e un file di risorse caricato al volo con jQuery.getScript. Funzionava, mi piaceva molto e, riletto oggi, ha lo stesso fascino di una radio a galena: ingegnoso, artigianale e decisamente superato.
Nel frattempo WordPress si è dotato di un sistema ufficiale per tradurre il JavaScript, che usa gli stessi strumenti e lo stesso flusso delle traduzioni PHP e che ha reso inutili quasi tutte le soluzioni fatte in casa. In questo articolo ti racconto come funziona oggi: perché wp_localize_script va usato per i dati e non per le stringhe, come si scrive codice traducibile con @wordpress/i18n, come si collegano le traduzioni con wp_set_script_translations e come si generano i file JSON con WP-CLI.
wp_localize_script: nato per tradurre, oggi serve per i dati
wp_localize_script() stampa nella pagina un oggetto JavaScript globale costruito a partire da un array PHP, e per anni è stato il modo standard per passare stringhe tradotte e dati agli script; oggi la documentazione ufficiale lo dice chiaramente, cioè che per passare dati la pratica migliore è wp_add_inline_script(), introdotta nella 4.5, mentre per le stringhe esiste un sistema di traduzione dedicato. Era la soluzione più vicina al mio esperimento del 2010, e infatti negli anni ne ho abusato con grande soddisfazione.
C’è anche un motivo tecnico per non affezionarsi troppo: wp_localize_script converte in stringa ogni valore scalare di primo livello, per cui un true arriva al browser come "1", un false come stringa vuota e i numeri come testo, mentre dalla 5.7 passare qualcosa che non sia un array genera un avviso che suggerisce proprio wp_add_inline_script(). Se ti serve consegnare configurazione a uno script, quindi, il modo pulito è questo:
<?php
// Passa la configurazione allo script mantenendo i tipi (booleani, numeri, array)
wp_enqueue_script( 'mio-script', plugins_url( 'build/index.js', __FILE__ ), array(), '1.0.0', true );
wp_add_inline_script(
'mio-script',
'const mioScriptConfig = ' . wp_json_encode( array(
'restUrl' => rest_url( 'mio-plugin/v1/' ),
'nonce' => wp_create_nonce( 'wp_rest' ),
'debug' => false,
) ) . ';',
'before' // stampato prima dello script, così è già disponibile
);Le stringhe da tradurre, invece, non dovrebbero più passare da qui: metterle in un array PHP significa tradurle lato server e poi spedirle al browser, separando il testo dal codice che lo usa e perdendo strumenti come i plurali e i contesti, che il sistema ufficiale gestisce in modo nativo.
Scrivere JavaScript traducibile con @wordpress/i18n
Il pacchetto @wordpress/i18n porta in JavaScript le stesse funzioni che usi in PHP, cioè __() per le stringhe semplici, _x() per quelle con un contesto, _n() e _nx() per i plurali e sprintf() per inserire valori variabili, e ti permette di scrivere il codice in inglese lasciando la traduzione a un passaggio successivo. Se compili con @wordpress/scripts non devi nemmeno preoccuparti di includere la libreria, perché il plugin di estrazione delle dipendenze trasforma l’import nel globale wp.i18n e aggiunge wp-i18n alle dipendenze nel file index.asset.php.
import { __, _n, sprintf } from '@wordpress/i18n';
// Stringa semplice: il secondo argomento è sempre il text domain
const label = __( 'Save settings', 'mio-plugin' );
// Plurale con valore variabile
const message = sprintf(
/* translators: %d: number of errors. */
_n( '%d error found', '%d errors found', count, 'mio-plugin' ),
count
);Il commento translators: sopra la stringa non è un vezzo, perché finisce nel file di traduzione e spiega a chi traduce che cosa rappresenta il segnaposto, evitando quelle traduzioni letterali che trasformano un pulsante in una frase senza senso. Se il codice JavaScript non passa da una build puoi usare direttamente il globale wp.i18n, ricordandoti di dichiarare wp-i18n tra le dipendenze dello script.
Collegare le traduzioni: wp_set_script_translations
wp_set_script_translations( $handle, $domain, $path ), disponibile dalla 5.0, dice a WordPress che uno script registrato ha delle traduzioni in un certo text domain e, facoltativamente, in quale cartella cercarle; al momento di stampare lo script WordPress carica il file JSON della lingua corrente e lo passa a wp.i18n prima che il tuo codice venga eseguito. La funzione aggiunge da sola la dipendenza wp-i18n e restituisce false se l’handle non è registrato, il che rende piuttosto facile capire perché, in certe giornate storte, le traduzioni non arrivano.
<?php
add_action( 'init', function () {
$asset = include plugin_dir_path( __FILE__ ) . 'build/index.asset.php';
wp_register_script(
'mio-script',
plugins_url( 'build/index.js', __FILE__ ),
$asset['dependencies'], // contiene già wp-i18n
$asset['version'],
true
);
// Text domain e cartella dei file JSON del plugin
wp_set_script_translations( 'mio-script', 'mio-plugin', plugin_dir_path( __FILE__ ) . 'languages' );
} );Il terzo argomento serve solo quando distribuisci le traduzioni con il plugin: se il plugin è ospitato su wordpress.org le traduzioni arrivano automaticamente con i language pack e WordPress le cerca da solo nella cartella delle lingue. Due casi particolari meritano una nota:
- per i blocchi registrati da
block.jsonla chiamata è automatica, a patto che il file contenga il campotextdomaine che lo script abbiawp-i18ntra le dipendenze; - per gli Script Modules, quelli usati per esempio dalla Interactivity API, dalla 7.0 esiste la funzione gemella
wp_set_script_module_translations(), che serve quando il modulo usa un text domain diverso da quello predefinito, cioè quasi sempre nel caso di un plugin.

I file JSON e il ruolo di WP-CLI
Le traduzioni JavaScript vivono in file JSON nel formato Jed, uno per ogni script e per ogni lingua, che si generano a partire dai normali file .po con il comando wp i18n make-json; il flusso completo parte da wp i18n make-pot, che estrae le stringhe da PHP, JavaScript, block.json e theme.json in un unico file .pot, prosegue con la traduzione in un file .po per lingua e si chiude con la conversione.
# 1. Estrae tutte le stringhe del plugin nel template .pot
wp i18n make-pot . languages/mio-plugin.pot
# 2. Dopo la traduzione (per esempio mio-plugin-it_IT.po),
# genera un JSON per ogni script che contiene stringhe tradotte
wp i18n make-json languages/ --no-purgeIl nome dei file JSON è la parte che fa impazzire tutti la prima volta: WordPress cerca prima {domain}-{locale}-{handle}.json e poi {domain}-{locale}-{md5}.json, dove l’hash MD5 è calcolato sul percorso dello script relativo alla cartella del plugin, considerando sempre il nome del file non minificato. Il comando make-json calcola l’hash per te, ma se sposti o rinomini lo script dopo aver generato le traduzioni l’hash non corrisponde più e le stringhe tornano magicamente in inglese; l’opzione --no-purge evita invece che le stringhe JavaScript vengano rimosse dal file .po di partenza.
Se una traduzione JavaScript non compare, nove volte su dieci il colpevole è il nome del file JSON, e la decima è il text domain scritto in modo diverso.
Quando qualcosa non torna, i filtri load_script_translation_file e pre_load_script_translations ti permettono di vedere quale file WordPress sta cercando e di intervenire, ed è un controllo che conviene fare prima di riscrivere mezzo plugin. Vale la pena ricordare, infine, che le ottimizzazioni sulle traduzioni arrivate con la 6.5, cioè i file .l10n.php generati con wp i18n make-php, riguardano i file .mo lato PHP e non toccano i JSON del JavaScript.
Tirando le somme
Tradurre il JavaScript in WordPress oggi è molto più semplice di quanto fosse nel 2010, a patto di usare gli strumenti giusti per lo scopo giusto: wp_add_inline_script, o in subordine wp_localize_script, per passare dati, @wordpress/i18n per scrivere stringhe traducibili, wp_set_script_translations per collegarle e WP-CLI per produrre i file JSON. Se vuoi approfondire, la guida all’internazionalizzazione del Block Editor Handbook e la pagina di wp_set_script_translations su developer.wordpress.org sono i due riferimenti da tenere nei preferiti, mentre nell’articolo su custom post type e tassonomie trovi un esempio di come le stesse regole valgono per le etichette lato PHP.
Resta, come scrivevo parlando di WordPress 7.0, il grande assente di sempre, cioè un vero supporto multilingua per i contenuti nel core, ma questa è un’altra storia e meriterebbe un articolo a parte. Se hai trovato altri tranelli con i file JSON o hai un sistema fatto in casa più ingegnoso del mio del 2010, raccontalo nei commenti e condividi l’articolo con chi sta impazzendo con un pulsante che si ostina a non tradursi; per altri spunti c’è la categoria WordPress, e se vuoi una mano su un progetto puoi prenotare una call conoscitiva gratuita o passare dai contatti.


non è un po limitante il fatto di avere un json “piatto” per la gestione delle label?
sarebbe nettamente più comodo permettere una struttura gerarchica del tipo:
var Localization = {
“errors” : {
“serverError”: “Errore del server”,
“validationError”: “Errore di validazione”
},
“notifications”: {
“addSuccess”:”Elemento aggiunto con successo”
},
“yes”: “Sì”
}
per una struttura di questo tipo mi vengono in mente almeno due soluzioni:
– utilizzare eval all’interno della funzione _e per sfruttare l’indice gerarchico “errors.serverError”
– utilizzare split(“.”) e ciclare sul vettore cosi ottenuto
Ciao Alberto,
il tuo approccio è sicuramente interessante ma se ho capito bene verrebbe meno l’apporoccio non ivasivo.
nella procedura di validazione io adesso scivo direttamente cose come questa:
_e(“Inserire una mail valida”)
_e(“Il campo cognome è obbligatorio”)
e se nel file di localizzazione non trovo le stringe viene visualizzato il testo passato come argomento.
Con il metodo da te proposto cosa dovrei scrivere?
_e(“errors.validationError”) ?
e se il file di localizzazione non è presente o non è completo?
fammi sapere se ho capito correttamente quella che è la tua idea…
Hmm dal tuo piccolo esempio mi era sembrato di capire che le chiavi dell’oggetto json fossero un po’ più “semplici” e non delle vere frasi.
Sono d’accordo sulla non invasività del tuo approccio, ma mi pare che sia poco scalabile in applicazioni di una certa entità soprattutto per il rischio di avere il file di localizzazione troppo disordinato.
Nel mio caso ovviamente in caso di file non presente o non completo l’applicazione lancierà un errore come credo sia giusto in caso di mancanza di una stringa.
Secondo me si tratta di due approcci diversi proprio per la loro natura.
Il mio mi sembra più “enterprise” con focus verso la scalabilità, l’ordine e una più forte regolamentazione mentre il tuo mi sembra più “easy” con focus verso la velocità di sviluppo e la non invasività 🙂
l’approccio è quello utilizzato anche nelle localizzazioni gettext con php (vedi qui e qui).
per la scalabilità io non vedo particolari problemi…
…o meglio forse può essere lungo andare a mantenere le traduzioni non avendo applicativi come POedit ma se ci facciamo aiutare da un piccolo script bash che ci prepara l’elenco delle stringhe da tradurre il più è fatto…
sbaglio?
no, no, non sbagli!
quello che non mi convince appieno è l’utilizzo di più “sistemi” per questa gestione quando è possibile sfruttare solamente JavaScript e una funzione di qualche riga per fare il tutto.
Però qua non stiamo discutendo più di efficacia o meno di una soluzione ma di aspetti più filosofici 🙂
Mi sembra molto buona come metodologia per avere dei messaggi di errore personalizzabili per la compilazione di form.