Une visiteuse d'une boutique en ligne espagnole tombe sur un bouton de paiement qui ne fait rien. Elle clique sur la petite icône de bug dans le coin et obtient « What's on your mind? » et un bouton marqué « Send ». Elle s'en sort. Mais elle écrit trois mots au lieu de trois phrases, parce que décrire un problème dans une langue étrangère demande un effort, et personne ne vous doit cet effort.
Jusqu'à cette semaine, c'était le cas de tous les sites non anglophones équipés de BugDump. Le widget parlait anglais, et le traduire voulait dire fournir soi-même les 36 chaînes. La plupart des gens ne le faisaient pas. Il est maintenant livré en anglais, russe, espagnol, français et portugais, et choisit sa langue tout seul. C'est ce choix qui s'est révélé intéressant.
Mise à jour du 3 octobre : l'allemand a suivi dans le SDK 0.0.42, comme sixième langue. Lui aussi a demandé des mots plus courts : le bouton de pièce jointe dit « Datei » (fichier) et le badge de note vocale « Audio ». L'entrée du journal des modifications donne les détails.

Trois endroits où un widget peut trouver une langue
Un widget intégré tourne sur la page de quelqu'un d'autre. Il peut lire trois signaux, et ils ne sont pas toujours d'accord.
L'attribut lang de la page. <html lang="es">, c'est le site qui dit dans quelle langue est son contenu. Les lecteurs d'écran s'en servent pour choisir la prononciation, et les navigateurs le consultent quand ils proposent de traduire une page. Il décrit la page, pas la personne qui la lit.
La liste des langues du navigateur. navigator.languages, ce sont les préférences du visiteur, dans l'ordre, quelque chose comme ["pt-BR", "pt", "en"]. navigator.language n'en est que la première entrée. Elle décrit la personne, pas la page.
Ce que le site dit au widget. Un réglage explicite dans le code d'intégration. Le plus fiable des trois, et celui que presque personne ne remplit.
Il y a aussi l'en-tête Accept-Language, que le serveur voit à chaque requête. Un widget chargé comme script depuis un CDN ne peut jamais le lire, il est donc exclu.
Pourquoi je commence par la page
Les widgets de chat de support suivent souvent le navigateur, et pour une conversation avec une personne, c'est logique : on veut parler dans sa propre langue. Un widget de rapport de bug, c'est autre chose. Il se trouve dans l'interface de quelqu'un d'autre, à côté de ses boutons et de ses textes, et il parle de son produit. Un panneau en français sur une page de paiement en anglais ressemble lui-même à un bug.
Voici donc l'ordre que j'ai choisi :
<html lang>, s'il désigne une langue que le widget embarque.Sinon, la première des langues du navigateur du visiteur que le widget embarque.
Sinon, l'anglais.
La page passe en premier pour qu'un site anglais reste en anglais, même pour un visiteur dont le navigateur est réglé en russe. Le navigateur vient en second pour qu'une page sans aucun attribut lang obtienne quand même un résultat sensé.
La faille évidente, c'est une page avec un mauvais lang. Beaucoup de templates de départ livrent <html lang="en"> et personne n'y touche jamais, si bien qu'un site entièrement rédigé en portugais peut quand même se déclarer anglais. J'ai décidé que la correction, c'est l'attribut, pas une devinette plus astucieuse. Un mauvais lang pénalise déjà les utilisateurs de lecteurs d'écran de ce site. Un widget qui le contourne en silence cache le problème au lieu de le faire remonter.
Comparer les tags de langue sans chercher à être malin
Les tags de langue prennent beaucoup de formes : fr, fr-CA, FR, ou pt_BR venant d'un backend qui n'a jamais entendu parler de BCP 47. Le widget embarque un jeu de chaînes par langue, donc la comparaison ne regarde que la partie avant le premier - ou _, en minuscules :
function findLocale(tag) {
const base = tag.toLowerCase().split(/[-_]/)[0];
return SHIPPED_LOCALES.find((locale) => locale === base);
}
Un visiteur au Portugal obtient donc du portugais brésilien. Ce n'est pas parfait, et je préfère le dire que faire semblant.
La recherche dans une liste est délibérée. base in LOCALES se lit presque pareil, jusqu'au jour où arrive un tag comme constructor, qui correspond à une propriété dont hérite chaque objet JavaScript. Personne ne met lang="constructor" exprès, mais du point de vue d'un widget, un attribut lang est une entrée venue d'un inconnu.
Ce qui a cassé en chemin
Un test qui dépendait de l'ordinateur qui le lançait. Les versions récentes de Node ont un navigator global, et son languages suit la locale de la machine. Mes tests de configuration tournent sans DOM, donc le premier jet aurait résolu « auto » en russe sur une machine réglée en russe et en anglais dans la CI. La détection ne renvoie désormais rien s'il n'existe pas aussi un document, ce qui correspond de toute façon à un environnement hors navigateur.
Le russe est long. « Прикрепить » (joindre) compte dix lettres là où l'anglais en a six, et il a poussé le troisième bouton de la barre d'actions sur une deuxième ligne. La correction n'est pas passée par le CSS, mais par un mot plus court : le bouton russe dit maintenant « Файл » (fichier) et laisse l'icône de trombone faire le reste. Le badge de note vocale sur les pièces jointes avait le même problème. C'est une vignette de 72 pixels, et « Nota de voz » y finit en points de suspension, donc l'espagnol et le français disent maintenant « Audio » et le portugais « Áudio ».
On ne trouve ces problèmes qu'en regardant. J'ai affiché le panneau dans chaque langue avant la sortie, et toutes les langues sauf l'anglais ont eu besoin d'au moins un mot plus court.
Des chaînes collées dans le code. « Uploading 1/3… 40% » était construit avec le mot en dur, tout comme « Microphone 2 » et « Powered by Bugdump ». Plutôt que d'inventer une syntaxe de template, j'ai réduit la partie traduisible au mot seul, et le code ajoute les nombres et le nom après. Ça marche parce que toutes les langues que le widget embarque placent le mot en premier. Le japonais, non, et le jour où je l'ajouterai, ces trois chaînes deviendront des templates.
Les lecteurs d'écran. Le widget s'affiche dans un shadow root sur la page hôte, il hérite donc du lang de la page. Un panneau français sur une page anglaise serait lu à voix haute avec une prononciation anglaise. Le widget pose désormais lang sur son propre élément.
Le site a toujours le dernier mot
La détection automatique est une valeur par défaut, pas une règle. Un site peut indiquer la langue dans son code d'intégration :
<script
src="https://bugdump.com/sdk/latest.js"
async
data-api-key="your-api-key"
data-locale="fr"
></script>
translations remplace toujours n'importe quelle chaîne par-dessus la langue choisie, si bien que remplacer « Envoyer un retour » par « Signaler un bug » fonctionne en français comme en anglais. Pour une langue que le widget n'embarque pas, locale: 'nl' avec vos propres chaînes en néerlandais fonctionne aussi, et tout ce que vous omettez retombe sur l'anglais.
Sur la page Widget de BugDump, le même choix est une liste déroulante, et l'aperçu en direct change de langue au fur et à mesure :

Ce que ça a coûté
Les cinq langues sont incluses dans le script. Les quatre langues supplémentaires ont ajouté environ 2,5 Ko gzippés, et le widget pèse maintenant environ 32 Ko. J'ai envisagé de les charger à la demande, mais cela veut dire une requête de plus et un panneau qui ne peut s'afficher qu'une fois la réponse revenue. Un état de chargement pour économiser deux kilo-octets et demi, c'est un mauvais calcul.
Ce que je n'ai pas construit : changer la langue d'un widget déjà affiché, et les mises en page de droite à gauche. Aucune des langues embarquées ne s'écrit de droite à gauche, et une single-page app qui change de langue peut démonter le widget et le relancer, en attendant que quelqu'un ait besoin de plus.
Si vous construisez vous-même un widget
Lisez
<html lang>avantnavigator.languages. Votre widget fait partie de la page.Comparez sur la langue de base, et déclarez la langue réellement utilisée avec votre propre attribut
lang.Laissez le site tout surcharger, et faites de la surcharge un simple attribut sur la balise script.
Affichez chaque chaîne dans chaque langue avant la sortie. Les traductions sont plus longues que prévu.
Le widget BugDump fait tout cela depuis le SDK 0.0.41, et les utilisateurs de la balise script l'ont déjà. Traduire et personnaliser le widget couvre toutes les options, et l'entrée du journal des modifications en donne la version courte.
Pour aller plus loin
Des widgets intégrés qui suivent le mode sombre de votre application — l'autre moitié du problème d'un widget sur la page d'un autre : suivre le bouton de thème de l'app hôte.
Sous-domaines par tenant dans TanStack Start avec les rewrites du routeur — une autre histoire de construction : servir chaque portail sur son propre sous-domaine sans déplacer une seule route.
Les trois quarts de mon SDK étaient du code que presque personne n'exécutait — comment le SDK du widget a perdu 76 % de son poids.
