Une extension de réservation de cours de yoga, traduite en anglais pour des clients à clientèle internationale, affiche du texte anglais correctement traduit sur l’écran d’administration, mais persiste à afficher les chaînes en français sur le site public, alors même que le visiteur navigue avec en_US comme langue active et que le fichier mon-extension-en_US.mo est bien présent dans le dossier de langues du plugin, correctement compilé depuis son fichier .po source.
Le développeur vérifie l’orthographe du nom de fichier, la présence du textdomain correct dans l’en-tête du plugin, la cohérence entre le domaine déclaré dans le code et celui utilisé dans les appels __(). Tout semble en ordre. Le problème est ailleurs : dans le moment précis où load_plugin_textdomain() est appelée par rapport au moment où les chaînes traduites sont réellement évaluées.
Le symptôme précis
Certaines chaînes de l’extension s’affichent bien traduites, d’autres non, sans logique de format apparente. En creusant, le développeur remarque que les chaînes traduites correctement sont toutes situées dans des fonctions appelées tardivement, pendant le rendu du gabarit, tandis que les chaînes qui restent en français sont toutes évaluées très tôt, notamment dans des propriétés de classe initialisées au chargement du fichier principal du plugin.
Le diagnostic

Le code fautif ressemble à ceci :
class Mon_Extension {
private $libelle_bouton = '';
public function __construct() {
$this->libelle_bouton = __( 'Réserver ma séance', 'mon-extension' );
add_action( 'init', array( $this, 'charger_traductions' ) );
}
public function charger_traductions() {
load_plugin_textdomain( 'mon-extension', false, dirname( plugin_basename( __FILE__ ) ) . '/langues' );
}
}
new Mon_Extension();
L’appel à __() dans le constructeur s’exécute au moment où le fichier principal du plugin est chargé par WordPress, ce qui se produit très tôt dans le cycle de requête, généralement avant même le hook plugins_loaded, et certainement bien avant init. Or, load_plugin_textdomain() n’est appelée, elle, que sur le hook init, dans charger_traductions(). Résultat : au moment où __( 'Réserver ma séance', 'mon-extension' ) s’exécute, aucun fichier de traduction n’est encore chargé pour ce domaine, la fonction retourne donc la chaîne source telle quelle, en français, et cette valeur reste figée dans la propriété $libelle_bouton pour le reste de la requête, même une fois les traductions chargées quelques instants plus tard.
Le correctif
La règle à retenir est simple mais stricte : ne jamais évaluer une chaîne traduisible avant que le textdomain correspondant ne soit chargé, ce qui signifie concrètement ne jamais appeler __() ou _e() dans un constructeur de classe instanciée tôt, ni dans le corps principal d’un fichier de plugin exécuté au chargement.
class Mon_Extension {
private $libelle_bouton = '';
public function __construct() {
add_action( 'init', array( $this, 'charger_traductions' ) );
add_action( 'init', array( $this, 'initialiser_libelles' ), 20 );
}
public function charger_traductions() {
load_plugin_textdomain( 'mon-extension', false, dirname( plugin_basename( __FILE__ ) ) . '/langues' );
}
public function initialiser_libelles() {
$this->libelle_bouton = __( 'Réserver ma séance', 'mon-extension' );
}
}
new Mon_Extension();
La priorité 20, supérieure à la priorité par défaut de 10 utilisée pour charger_traductions(), garantit que le chargement du textdomain s’exécute bien avant l’évaluation des libellés, les deux étant accrochés au même hook init mais dans un ordre désormais explicite et non laissé au hasard de l’ordre d’ajout des actions.
Une meilleure pratique encore : différer totalement l’évaluation
Une approche encore plus sûre consiste à ne jamais stocker une chaîne traduite dans une propriété évaluée une seule fois, mais à toujours appeler __() directement au moment du rendu, dans la méthode qui affiche réellement le libellé :
public function afficher_bouton_reservation() {
echo esc_html__( 'Réserver ma séance', 'mon-extension' );
}
Cette forme élimine complètement le risque d’évaluation précoce, puisque la traduction est recalculée à chaque affichage, un coût négligeable pour une simple chaîne de caractères, largement compensé par la garantie de toujours refléter l’état correct des traductions chargées.
Le cas particulier des extensions sur WordPress.org
Depuis WordPress 4.6, pour les extensions hébergées sur le répertoire officiel WordPress.org, le cœur de WordPress peut charger automatiquement les traductions fournies par le système de traduction communautaire du projet, sans même nécessiter d’appel explicite à load_plugin_textdomain() dans certains cas, à condition que le plugin déclare correctement son en-tête Text Domain et ne fournisse pas déjà de fichiers .mo personnalisés en conflit. Cette automatisation ne dispense cependant pas de connaître le mécanisme sous-jacent, notamment pour les extensions distribuées en dehors du répertoire officiel, où l’appel manuel reste nécessaire.
Une traduction qui existe sur le disque mais jamais chargée au bon moment n’est, du point de vue de l’utilisateur final, pas différente d’une traduction absente.
En résumé
Ce bug d’apparence mystérieuse se résume, une fois compris, à une simple question d’ordre d’exécution : toute chaîne traduisible évaluée avant l’appel à load_plugin_textdomain() restera figée dans sa langue source pour le reste de la requête. Différer systématiquement l’évaluation des chaînes traduites jusqu’au moment réel de leur affichage élimine cette classe entière de bugs, sans jamais avoir à se soucier de l’ordre exact des hooks.