"Please fill in this field" s’affiche dans un message d’erreur retourné par une route REST personnalisée, sur un site entièrement rédigé en français, dont l’équipe jurerait avoir traduit chaque chaîne du plugin concerné. Les fichiers .po et .mo sont bien présents dans le dossier languages, la locale du site est correctement réglée sur fr_FR. Et pourtant, rien ne se traduit côté API.
Symptôme
Un contrôleur REST personnalisé, développé pour un plugin maison, retourne des messages construits avec __() ou _e() à l’intérieur de ses callbacks et de ses validations d’arguments. Sur l’interface d’administration classique, ces mêmes chaînes s’affichent bien en français. Mais dès qu’elles transitent par une route REST, elles ressortent en anglais, langue source du code, comme si la fonction de traduction n’avait jamais été appelée.
Diagnostic
Le mécanisme de traduction de WordPress ne fonctionne jamais tout seul : une chaîne enveloppée dans __( 'texte', 'mon-domaine' ) n’est traduite que si le domaine de texte mon-domaine a préalablement été chargé via load_plugin_textdomain() ou load_theme_textdomain(), et que ce chargement a eu lieu avant l’exécution du code qui appelle __(). Sur l’administration classique, ce chargement se produit naturellement au hook init, largement avant l’affichage de la page. Sur une requête REST, le cycle d’exécution est plus court et plus ciblé : si le plugin charge son domaine de texte uniquement depuis un hook lié à l’affichage d’une page (comme template_redirect), ce hook ne se déclenche jamais lors d’un appel direct à /wp-json/..., puisqu’aucune page classique n’est générée dans ce contexte.
Le contrôleur REST s’exécute donc bel et bien, la fonction __() est bien appelée, mais le domaine de texte associé n’a jamais été chargé en mémoire au moment de cet appel. WordPress se rabat alors silencieusement sur la chaîne source, en anglais, sans lever la moindre erreur.
Correctif

add_action( 'init', function () {
load_plugin_textdomain( 'mon-domaine', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
} );
Accrocher le chargement du domaine de texte au hook init plutôt qu’à un hook spécifique à l’affichage garantit qu’il est disponible quel que soit le point d’entrée emprunté par la requête suivante : administration, front classique, ou route REST. Le hook init se déclenche systématiquement, y compris pour les requêtes traitées par rest_api_init, qui se produit après.
Depuis WordPress 6.5, le mécanisme de chargement des traductions accepte également des fichiers au format .l10n.php, générés à partir des fichiers .mo classiques par les outils de traduction récents. Ce format, un simple tableau PHP retourné par un fichier, se charge plus vite qu’un fichier .mo analysé à chaque requête, ce qui profite particulièrement à un contexte REST où chaque milliseconde de traitement compte. Le principe de chargement reste toutefois identique : ce format ne change rien au fait que load_plugin_textdomain() doit être appelé avant la génération du texte, seulement à la rapidité de la lecture du fichier une fois cet appel effectué.
Prévention
- Toujours accrocher le chargement d’un domaine de texte à
init, jamais à un hook conditionné à l’affichage d’une page classique - Vérifier le comportement des chaînes traduites spécifiquement via un appel direct à la route REST concernée, pas seulement via l’administration
- Sur un plugin distribué, garder le nom du domaine de texte strictement identique entre le fichier d’en-tête du plugin et chaque appel à
__()
En résumé
Un texte non traduit dans une réponse REST n’est presque jamais un problème de fichier de traduction manquant ; c’est bien plus souvent un problème de moment de chargement. Le domaine de texte doit être en mémoire avant que la fonction __() ne soit appelée, ce qui impose de le charger sur un hook qui se déclenche pour tous les types de requêtes, y compris celles qui ne passent jamais par le rendu classique d’une page.