Das Problem

Du willst deinen Markennamen im Header optisch auflockern. “Rebel Alliance” soll so aussehen:

<h1 data-i18n="brand_name">
  <span class="brand-light">Rebel</span>
  <span class="brand-bold"> Alliance</span>
</h1>
.brand-light  { color: rgba(255,255,255,0.9); font-weight: 300; }
.brand-bold   { background: linear-gradient(90deg, #4a9eff, #00d4ff);
                -webkit-background-clip: text;
                -webkit-text-fill-color: transparent;
                font-weight: 800; }

Im Browser-Preview sieht es perfekt aus. Du öffnest die echte Seite — und der Schriftzug ist wieder einheitlich grau. Die Spans sind weg.


Die Diagnose

In den DevTools: Element inspizieren. Statt der zwei Spans steht da nur noch:

<h1>Rebel Alliance</h1>

Kein data-i18n-Attribut mehr? Doch, das ist noch da. Aber die Kinder-Elemente — weg.

Der Verdächtige ist die i18n-Funktion. Ein schneller Blick in language-manager.js:

const elements = document.querySelectorAll('[data-i18n]');
elements.forEach(element => {
  const key = element.getAttribute('data-i18n');
  const translation = translations[currentLang][key];
  if (translation) {
    element.textContent = translation;  // ← hier!
  }
});

Zeile gefunden.


Die Ursache: textContent vs innerHTML

element.textContent = 'neuer Text' macht genau das, was der Name sagt: es setzt den Text-Inhalt des Elements. Was dabei passiert:

  1. Alle Kind-Elemente werden gelöschttextContent ersetzt das gesamte DOM-Subtree
  2. Ein einzelner Text-Node wird erstellt mit dem neuen Wert
  3. HTML-Entities werden escaped<span> wird zu &lt;span&gt; (kein XSS-Risiko, aber auch kein HTML)

Das ist kein Bug in der Library — es ist korrektes, sicheres Verhalten. textContent ist XSS-safe, weil es niemals HTML interpretiert. Aber es ist auch rücksichtslos gegenüber bestehenden Kind-Elementen.

Vergleich:

MethodeKinder-ElementeXSS-sicherHTML möglich
textContent =werden gelöscht
innerHTML =werden gelöscht⚠️ nur mit Sanitizer
innerText =werden gelöscht
Nur Text-Nodes ersetzenbleiben erhalten

Die Lösung: Drei Ansätze

Ansatz 1 (einfachst): data-i18n vom Parent entfernen

Wenn der Element-Text ein Markenname ist, der sich ohnehin nie ändert (z.B. “Rebel Alliance” bleibt in DE und EN gleich), brauchst du gar kein data-i18n:

<!-- Vorher -->
<h1 data-i18n="brand_name">
  <span class="brand-light">Rebel</span>
  <span class="brand-bold"> Alliance</span>
</h1>

<!-- Nachher: data-i18n entfernt, Spans bleiben -->
<h1>
  <span class="brand-light">Rebel</span>
  <span class="brand-bold"> Alliance</span>
</h1>

✅ Einfach, kein JS nötig ⚠️ Nur sinnvoll wenn der Text nicht übersetzt wird


Ansatz 2: data-i18n auf die Spans, nicht den Parent

Statt das Parent zu übersetzen, übersetzt du jede Span einzeln:

<h1>
  <span class="brand-light" data-i18n="brand_part1">Rebel</span>
  <span class="brand-bold"  data-i18n="brand_part2"> Alliance</span>
</h1>
// translations.js
'brand_part1': 'Rebellen',
'brand_part2': ' Allianz',

✅ Übersetzung funktioniert ✅ Spans bleiben erhalten ⚠️ Mehr Keys in der Translations-Datei


Ansatz 3: i18n-Library patchen (nur Text-Nodes ersetzen)

Wenn du die Library kontrollierst, kannst du sie so umschreiben, dass nur Text-Nodes ersetzt werden — Kind-Elemente bleiben unangetastet:

function setTranslation(element, translation) {
  // Suche den ersten direkten Text-Node
  const textNode = Array.from(element.childNodes)
    .find(node => node.nodeType === Node.TEXT_NODE);

  if (textNode) {
    // Nur den Text-Node ersetzen, Kinder-Elemente bleiben
    textNode.textContent = translation;
  } else if (element.children.length === 0) {
    // Kein Text-Node, keine Kinder → direkt setzen
    element.textContent = translation;
  }
  // Hat Kinder-Elemente aber keinen eigenen Text-Node → nichts tun
}

✅ Beste Lösung wenn Übersetzung + Styling kombiniert werden muss ⚠️ Erfordert Anpassung der i18n-Library


Lessons Learned

1. textContent ist destruktiv, nicht additiv. Es ersetzt das gesamte Subtree. Wenn dein Element Kind-Elemente hat, sind sie danach weg. Immer.

2. i18n-Libraries machen das bewusst so. textContent statt innerHTML ist eine Sicherheitsentscheidung (XSS-Prävention). Du kannst das Verhalten nicht durch Konfiguration ändern — du musst die Datenstruktur anpassen.

3. Marken-Namen brauchen oft gar kein i18n. “Rebel Alliance” heißt auf Englisch auch “Rebel Alliance”. Bevor du ein Element in dein Translations-System aufnimmst, frage: Wird dieser Text wirklich übersetzt?

4. HTML-Struktur und Übersetzungs-Keys haben eine 1:1-Beziehung. Wenn du einen H1 in zwei verschieden gestylte Hälften aufteilst, brauchst du zwei Übersetzungs-Keys — nicht einen für den Parent.


Schnell-Checkliste

Bevor du data-i18n auf ein Element setzt:

  • Hat das Element Kind-Elemente mit eigenem Styling? → Ansatz 2 oder 3
  • Wird der Text tatsächlich übersetzt? → Falls nein: data-i18n weglassen
  • Nutzt deine i18n-Library textContent? → grep -r "textContent" js/ prüfen