Ein eingebettetes Widget lokalisieren: Seitensprache oder Browsersprache?

widget

In einem spanischen Onlineshop findet eine Kundin einen Checkout-Button, der nichts tut. Sie klickt auf das kleine Bug-Symbol in der Ecke und bekommt „What's on your mind?“ und einen Button mit „Send“. Sie kommt damit zurecht. Aber sie schreibt drei Wörter statt drei Sätzen, denn ein Problem in einer Fremdsprache zu beschreiben ist Arbeit, und diese Arbeit schuldet dir niemand.

Bis diese Woche galt das für jede nicht englischsprachige Website mit BugDump. Das Widget sprach Englisch, und um es zu übersetzen, musstest du alle 36 Strings selbst übergeben. Die meisten haben das nicht getan. Jetzt bringt es Englisch, Russisch, Spanisch, Französisch und Portugiesisch mit und wählt selbst eine Sprache aus. Genau diese Auswahl war am Ende der interessante Teil.

Nachtrag vom 3. Oktober: Deutsch kam mit SDK 0.0.42 als sechste Sprache dazu. Auch dafür brauchte es kürzere Wörter: Der Button zum Anhängen heißt „Datei“ und das Badge für Sprachnotizen „Audio“. Die Details stehen im Changelog-Eintrag.

Das Widget für Fehlerberichte von BugDump auf Französisch, Spanisch, Portugiesisch und Russisch

Drei Quellen, aus denen ein Widget seine Sprache erfährt

Ein eingebettetes Widget läuft auf einer fremden Seite. Es kann drei Signale lesen, und die stimmen nicht immer überein.

Das lang-Attribut der Seite. <html lang="es"> sagt, in welcher Sprache der Inhalt der Website geschrieben ist. Screenreader wählen danach die Aussprache, und Browser schauen darauf, wenn sie anbieten, eine Seite zu übersetzen. Es beschreibt die Seite, nicht die Person, die sie liest.

Die Sprachliste des Browsers. navigator.languages enthält die bevorzugten Sprachen der Person am Bildschirm in ihrer Reihenfolge, etwa ["pt-BR", "pt", "en"]. navigator.language ist nur der erste Eintrag. Es beschreibt die Person, nicht die Seite.

Was die Website dem Widget vorgibt. Eine explizite Einstellung im Embed-Code. Die zuverlässigste der drei Quellen und die, die fast niemand ausfüllt.

Dazu kommt noch der Accept-Language-Header, den ein Server bei jedem Request sieht. Ein Widget, das als Script von einem CDN geladen wird, kommt nie an ihn heran, also fällt er weg.

Warum ich mit der Seite anfange

Support-Chat-Widgets folgen oft dem Browser, und für ein Gespräch mit einem Menschen ist das sinnvoll: Du willst in deiner eigenen Sprache reden. Ein Widget für Fehlerberichte ist etwas anderes. Es sitzt in einer fremden Oberfläche, neben deren Buttons und Texten, und es geht um deren Produkt. Ein französisches Panel auf einer englischen Checkout-Seite sieht selbst wie ein Bug aus.

Die Reihenfolge, die ich gewählt habe, ist also:

  1. <html lang>, wenn es eine Sprache nennt, die das Widget mitbringt.

  2. Sonst die erste der Browsersprachen, die es mitbringt.

  3. Sonst Englisch.

Die Seite kommt zuerst, damit eine englische Website englisch bleibt, auch wenn der Browser auf Russisch eingestellt ist. Der Browser kommt als Zweites, damit eine Seite ganz ohne lang-Attribut trotzdem etwas Vernünftiges bekommt.

Die offensichtliche Lücke ist eine Seite mit falschem lang. Viele Starter-Templates liefern <html lang="en"> aus, und niemand fasst es je an, also kann eine komplett portugiesische Website trotzdem behaupten, englisch zu sein. Ich habe entschieden, dass die Lösung dafür das Attribut ist, nicht eine schlauere Vermutung. Ein falsches lang schadet schon jetzt allen, die diese Website mit einem Screenreader nutzen. Ein Widget, das es stillschweigend umgeht, versteckt das Problem, statt es sichtbar zu machen.

Tags abgleichen, ohne schlau sein zu wollen

Sprach-Tags gibt es in vielen Formen: fr, fr-CA, FR oder pt_BR aus einem Backend, das nie von BCP 47 gehört hat. Ich liefere einen Satz Strings pro Sprache aus, also schaut der Abgleich nur auf den Teil vor dem ersten - oder _, in Kleinbuchstaben:

JavaScript
function findLocale(tag) {
  const base = tag.toLowerCase().split(/[-_]/)[0];
  return SHIPPED_LOCALES.find((locale) => locale === base);
}

Wer in Portugal sitzt, bekommt also brasilianisches Portugiesisch. Das ist nicht perfekt, und ich sage das lieber offen, als so zu tun, als wäre es anders.

Die Suche in der Liste ist Absicht. base in LOCALES liest sich fast genauso, bis ein Tag wie constructor ankommt und auf eine Eigenschaft trifft, die jedes JavaScript-Objekt erbt. Niemand setzt absichtlich lang="constructor", aber aus Sicht eines Widgets ist ein lang-Attribut eine Eingabe von Fremden.

Was unterwegs kaputtging

Ein Test, der davon abhing, auf wessen Laptop er lief. Neuere Node-Versionen haben ein globales navigator, und dessen languages folgt dem Locale der Maschine. Meine Konfigurationstests laufen ohne DOM, also hätte der erste Entwurf „auto“ auf einer russisch eingestellten Maschine zu Russisch aufgelöst und in der CI zu Englisch. Die Erkennung gibt jetzt nichts zurück, solange nicht auch ein document existiert, und so sollte eine Umgebung ohne Browser ohnehin aussehen.

Russisch ist lang. „Прикрепить“ (anhängen) hat zehn Buchstaben, wo das Englische sechs hat, und schob den dritten Button der Aktionsleiste in eine zweite Zeile. Die Lösung war kein CSS, sondern ein kürzeres Wort: Der russische Button heißt jetzt „Файл“ (Datei), den Rest erledigt das Büroklammer-Symbol. Das Badge für Sprachnotizen an Anhängen hatte dasselbe Problem. Es ist eine Kachel von 72 Pixeln, und „Nota de voz“ endet dort mit Auslassungspunkten, deshalb steht auf Spanisch und Französisch jetzt „Audio“ und auf Portugiesisch „Áudio“.

Solche Dinge findest du nur, wenn du hinschaust. Ich habe das Panel vor dem Release in jeder Sprache gerendert, und jede Sprache außer Englisch brauchte mindestens ein kürzeres Wort.

Im Code zusammengeklebte Strings. „Uploading 1/3… 40%“ wurde mit dem fest eingebauten Wort zusammengesetzt, genauso „Microphone 2“ und „Powered by Bugdump“. Statt eine Template-Syntax zu erfinden, ist der übersetzbare Teil jetzt nur das Wort, und der Code hängt die Zahlen und den Namen dahinter. Das funktioniert, weil jede Sprache, die ich ausliefere, das Wort an den Anfang stellt. Japanisch täte das nicht, und an dem Tag, an dem es dazukommt, werden diese drei Strings zu Templates.

Screenreader. Das Widget rendert in einem Shadow Root auf der Host-Seite und erbt deshalb das lang der Seite. Ein französisches Panel auf einer englischen Seite würde mit englischer Aussprache vorgelesen. Deshalb setzt das Widget jetzt lang auf seinem eigenen Element.

Die Website entscheidet trotzdem

Die automatische Erkennung ist eine Voreinstellung, keine Regel. Eine Website kann die Sprache in ihrem Embed-Code festlegen:

HTML
<script
  src="https://bugdump.com/sdk/latest.js"
  async
  data-api-key="your-api-key"
  data-locale="fr"
></script>

translations ersetzt weiterhin jeden String, egal welche Sprache gewählt wurde. „Send feedback“ in „Report a bug“ umzubenennen funktioniert also auf Französisch genauso wie auf Englisch. Für eine Sprache, die ich nicht mitliefere, funktioniert auch locale: 'nl' plus deine eigenen niederländischen Strings, und alles, was du weglässt, fällt auf Englisch zurück.

Auf der Seite „Widget“ in BugDump ist dieselbe Wahl ein Dropdown, und die Live-Vorschau wechselt die Sprache, während du auswählst:

Die Einstellung „Sprache“ auf der Widget-Seite von BugDump, aufgeklappt mit Automatisch erkennen, English, Русский, Español, Français, Português und Deutsch

Was es gekostet hat

Alle fünf Sprachen sind im Script gebündelt. Die vier zusätzlichen brachten etwa 2,5 KB gzipped, und das Widget wiegt jetzt etwa 32 KB. Ich habe überlegt, sie bei Bedarf nachzuladen, aber das heißt ein Request mehr und ein Panel, das erst rendern kann, wenn die Antwort da ist. Ein Ladezustand, um zweieinhalb Kilobyte zu sparen, ist ein schlechter Tausch.

Was ich nicht gebaut habe: die Sprache eines Widgets wechseln, das schon auf dem Bildschirm ist, und Layouts von rechts nach links. Keine der Sprachen, die ich ausliefere, wird von rechts nach links geschrieben, und eine Single-Page-App, die die Sprache wechselt, kann das Widget abbauen und neu starten, bis jemand mehr braucht.

Wenn du selbst ein Widget baust

  • Lies <html lang> vor navigator.languages. Dein Widget ist Teil der Seite.

  • Gleiche nur die Basissprache ab und gib die Sprache, die du tatsächlich verwendest, mit einem eigenen lang-Attribut an.

  • Lass die Website alles überschreiben, und mach das Überschreiben zu einem einzigen Attribut am Script-Tag.

  • Rendere jeden String in jeder Sprache, bevor du auslieferst. Übersetzungen sind länger, als du denkst.

Das Widget von BugDump macht all das ab SDK 0.0.41, und wer es per Script-Tag einbindet, hat es schon. Alle Optionen beschreibt Das Widget übersetzen und an deine Marke anpassen, die Kurzfassung steht im Changelog-Eintrag.

Mehr zum Thema