Entwicklerhandbuch¶
Architektur¶
Frontend-Request
│
├─ POST /nt-lingua/transform ─→ TransformApiMiddleware
│ │
│ ├─ isLanguageTarget(target) = true ─→ TextTransformationService::translateStrings()
│ └─ isLanguageTarget(target) = false ─→ TextTransformationService::transformField()
│
└─ GET Seitenaufruf (Cookie/Param)
└─ TransformProcessor (DataProcessing) ─→ transformField() [Server-Fallback]
TextTransformationService
├─ einfache / leichte ─→ AiProviderInterface (nt_ai) + Prompts
└─ Sprachcodes ─→ TranslatorInterface (LLM/DeepL) + Glossar
TextCacheRepository
└─ tx_ntlingua_text_cache (UNIQUE: content_hash + target)
Zwei Kopplungspunkte:
- Frontend → Server:
POST /nt-lingua/transformwird vonTransformApiMiddlewareverarbeitet - Server → KI/Übersetzung:
TranslatorInterface(LLM oder DeepL) +AiProviderInterface(nt-ai)
nt_lingua kennt keine konkreten KI-Provider — der einzige externe Kopplungspunkt ist Netthinks\NtAi\Provider\AiProviderInterface.
Antwortformat und Überwachung¶
POST /nt-lingua/transform mit einem Sprachziel antwortet:
{
"target": "fr",
"translations": { "Quelltext": "Übersetzung" },
"untranslated": 0,
"degraded": false
}
| Feld / Kopfzeile | Bedeutung |
|---|---|
translations |
Zuordnung Quelltext → Übersetzung. Für ein Segment ohne Übersetzung steht hier der Quelltext, damit die Seite vollständig bleibt. |
untranslated |
Anzahl der Segmente ohne Übersetzung |
degraded |
true, sobald mindestens ein Segment gescheitert ist |
X-NtLingua-Untranslated |
Dieselbe Anzahl als Kopfzeile — für eine Prüfung ohne JSON-Auswertung |
Statuscodes: Ein Teilausfall antwortet 200 (der übersetzte Teil ist brauchbar), ein vollständiger Ausfall 503. Für die Anzeige macht das keinen Unterschied — das Client-Skript wertet den Status nicht aus, es wendet an, was es bekommt, und lässt die Seite sonst in der Ausgangssprache stehen.
Daneben gelten die Statuscodes der Budget- und Auslastungsgrenzen: 429 bei erschöpftem Kontingent, 503 mit Retry-After, wenn alle gleichzeitigen Anbieter-Anfragen belegt sind. Eine Überwachung sollte deshalb untranslated bzw. die Kopfzeile auswerten und nicht allein den Status.
Der Grund für diese Signale: Ohne sie ist ein Ausfall von außen nicht erkennbar. Die Antwort enthält dann die Quelltexte und sieht wie eine vollständige Übersetzung aus.
Gescheiterte Segmente werden nicht zwischengespeichert — der nächste Aufruf versucht es erneut. Zusätzlich erhält jeder Ausfall einen Eintrag in tx_ntlingua_log, dessen Feld error die Anzahl nennt. Dort gezählt wird der Anbieter-Aufruf, also nur die nicht bereits zwischengespeicherten Segmente; die anfragebezogene Zahl steht in untranslated.
Datenmodell¶
tx_ntlingua_text_cache¶
| Spalte | Typ | Beschreibung |
|---|---|---|
content_hash |
varchar(64) | sha256(normalize(source) \| target \| model) |
target |
varchar(20) | einfache, leichte oder Sprachcode (z. B. en) |
source_lang |
varchar(10) | Quellsprache (Standard: de) |
source_text |
mediumtext | Ursprünglicher Text |
result_text |
mediumtext | Transformierter/übersetzter Text |
provider |
varchar(30) | KI-Anbieter oder deepl |
model |
varchar(80) | Verwendetes Modell |
tokens_used |
int | Verbrauchte Token |
readability |
decimal(5,2) | Ø Wörter/Satz (niedriger = einfacher) |
source_uid |
int | UID des Quell-Datensatzes |
source_table |
varchar(80) | Tabelle des Quell-Datensatzes |
source_field |
varchar(80) | Feld des Quell-Datensatzes |
crdate / tstamp |
int | Unix-Timestamps |
Unique-Key: (content_hash, target) — Einfache, Leichte und alle Sprachcodes teilen sich dieselbe Tabelle kollisionsfrei.
Cache-Invalidierung: AfterDatabaseOperationsEvent → InvalidateTextCache löscht alle Einträge zum betroffenen Datensatz, wenn header oder bodytext gespeichert werden.
tx_ntlingua_glossary¶
| Spalte | Beschreibung |
|---|---|
source_term |
Quellbegriff (vor Vergleich normalisiert) |
target_lang |
Zielsprache (ISO-Code) oder * für alle Sprachen |
target_term |
Festgelegte Übersetzung (bei ignore-Modus leer lassen) |
mode |
fixed (erzwingen) oder ignore (unverändert lassen) |
Glossar-Einträge werden vor Cache und Provider ausgewertet. target_lang='*' gilt sprachunabhängig für alle DOM-Übersetzungen.
tx_ntlingua_ratelimit¶
Zähler mit festen Zeitfenstern für die öffentlichen Endpunkte (seit 1.1.0). Eine Zeile je Dimension und Fenster (bucket), verbrauchte Einheiten (used) und Löschzeitpunkt (expires). Geändert wird nur über ein bedingtes UPDATE … SET used = used + n WHERE used <= max - n; parallele Requests können ein Limit daher nicht überschreiten.
tt_content / pages (Erweiterungsfelder)¶
| Feld | Beschreibung |
|---|---|
tx_ntlingua_mt |
1 = maschinell übersetzt (readonly im Backend) |
tx_ntlingua_srchash |
Hash der Quellfelder zum Zeitpunkt der Overlay-Erzeugung — ermöglicht Delta-Erkennung |
CLI-Referenz¶
ntlingua:warmup¶
| Option | Standard | Beschreibung |
|---|---|---|
--target |
einfache |
einfache, leichte oder ISO-Sprachcode |
--limit |
0 |
Max. Datensätze (0 = alle) |
Verarbeitet alle tt_content-Datensätze mit sys_language_uid=0, deleted=0, hidden=0. Cache-Hits werden übersprungen — der Befehl ist idempotent und kann als nächtlicher Scheduler-Task ausgeführt werden.
# Einfache Sprache für alle Content-Elemente vorwärmen:
vendor/bin/typo3 ntlingua:warmup --target=einfache
# Englische Übersetzung, maximal 500 Datensätze:
vendor/bin/typo3 ntlingua:warmup --target=en --limit=500
ntlingua:cache-purge¶
Löscht gecachte Übersetzungen der angegebenen Zielsprachen (seit 1.4.3), damit der nächste ntlingua:warmup-dom sie neu übersetzt. Für Ergebnisse, die falsch sind, obwohl der Aufruf gelang — der Cache-Schlüssel kennt Text, Ziel und Modell, nicht den Prompt. --target ist Pflicht; ein Löschen aller Sprachen gibt es bewusst nicht.
ntlingua:overlays¶
| Option | Beschreibung |
|---|---|
--language |
sys_language_uid der Zielsprache (muss in TYPO3-Site-Konfiguration vorhanden sein) |
--code |
ISO-Sprachcode (z. B. en, fr) |
Erzeugt verbundene l10n-Overlays (connected mode). Bei Wiederholung werden nur geänderte Datensätze verarbeitet (Delta-Erkennung via tx_ntlingua_srchash).
Übersetzte Felder:
| Tabelle | Felder |
|---|---|
pages |
title, nav_title, subtitle, seo_title, description |
tt_content |
header, subheader, bodytext |
Warning
ntlingua:overlays im Live-Workspace ausführen, nicht in Draft-Workspaces.
JavaScript-API¶
// Sprache oder Modus wechseln:
NtLingua.transform('fr', buttonElement); // DOM-Übersetzung
NtLingua.transform('einfache', btn); // Einfache Sprache
NtLingua.transform('de'); // Reset zur Ausgangssprache
// Aktuellen Zustand abfragen:
NtLingua.getCurrent(); // z. B. 'de', 'fr', 'einfache'
Custom Events¶
document.addEventListener('NtLingua:ready', (e) => {
console.log('Initialisiert, aktives Ziel:', e.detail.target);
});
document.addEventListener('NtLingua:done', (e) => {
console.log('Umschaltung abgeschlossen:', e.detail.target);
});
Eigenen Übersetzer hinzufügen¶
TranslatorInterface implementieren und in Services.yaml als Alias registrieren:
namespace MyVendor\MyExt\Translator;
use Netthinks\NtLingua\Translator\TranslatorInterface;
final class MyTranslator implements TranslatorInterface
{
public function translateBatch(array $texts, string $sourceLang, string $targetLang): array { … }
public function getName(): string { return 'my-translator'; }
public function getModel(): string { return 'my-model'; }
}
# Services.yaml der eigenen Extension:
Netthinks\NtLingua\Translator\TranslatorInterface:
alias: MyVendor\MyExt\Translator\MyTranslator
Fluid-Wrapper (Pflicht für JS-Feldtausch)¶
Content-Felder müssen in FSC-Template-Overrides mit data-ntlingua-*-Attributen umschlossen werden:
<!-- bodytext (z. B. in einem EXT:fluid_styled_content Override): -->
<div data-ntlingua-uid="{data.uid}" data-ntlingua-field="bodytext">
<f:format.html>{data.bodytext}</f:format.html>
</div>
<!-- header: -->
<div data-ntlingua-uid="{data.uid}" data-ntlingua-field="header">
{data.header}
</div>
Fluid-Overrides unter packages/netthinks/Resources/Private/Extensions/fluid_styled_content/ anlegen.
Sicherheit der öffentlichen Endpunkte¶
ContentAccessGuardentscheidet mit den Regeln des Frontends (PageRepository,RootlineUtility,RecordAccessVoter), ob ein Inhaltselement bzw. eine Seite für einen anonymen Request verwendet werden darf: versteckt, Start-/Stoppzeit, Zugriffsgruppen, „Auf Unterseiten ausdehnen", Site und Sprache. Vor dieser Prüfung wird nichts an einen Anbieter gegeben.TextTransformationService::transformField()liefert für HTML-Quellen bereinigtes HTML und für Textquellen reinen Text — unabhängig von der Antwort des Anbieters; Cache-Treffer werden bei der Ausgabe erneut bereinigt. Wer Text in HTML schreibt, muss escapen (TransformApiMiddlewaresendetformat,EinfacheSpracheMiddlewareescaped vor dem Wiedereinsetzen der Original-Tags).- Öffentliche Aufrufer übergeben ein
ProviderBudget(ausPublicUsageLimits::budgetFor()); reserviert wird unmittelbar vor jedem Anbieter-Aufruf, auch vor dem LLM-Fallback. Backend-/CLI-Code übergibt keines; die Übersetzer verhalten sich dann genau wie ohne Budgets. - Übersetzer, die je Stapel mehr als einmal senden können, implementieren
BudgetAwareTranslatorInterfaceund reservieren selbst vor jedem Senden (LlmTranslator: jeder Teilblock, jede Wiederholung eines leeren Teilblocks und seit 1.4.2 jede Hälfte eines Teilblocks, dessen Antwort nicht passte — falsche Länge, kein JSON-Array oder ein Element, das kein Text ist; halbiert wird, bis die Antwort passt oder ein einzelner Text übrig ist, der dannnullbleibt; lehnt das Budget ab, endet das Aufteilen und Erhaltenes bleibt;DeepLTranslator: jede Anfrage, auch Wiederholungen nach 429). Vertrag:BudgetExceededExceptionheißt, der Stapel hat nichts ergeben; eine Ablehnung, nachdem schon gesendet wurde, liefert den Stapel mit unübersetzten Einträgen als Quelltext zurück —TextTransformationServicegibt diese weder aus noch cacht es sie. Eine einfacheTranslatorInterface-Implementierung gilt als ein einziger Sendevorgang und wird vorab einmal reserviert. RateLimitedProviderBudgetbelegt bei der ersten Reservierung außerdem einen vonmaxConcurrentProviderRequestsPlätzen (ConcurrencySlots, Zeilenslot_<n>intx_ntlingua_ratelimit, belegt per bedingtemUPDATEoder durch das anlegendeINSERT) und hält ihn bisrelease(), zur Zerstörung des Objekts oder bis zum Ablauf der Belegungsdauer. Sind alle belegt, folgtConcurrencyLimitExceededException(eineBudgetExceededException); die API antwortet mit 503. Wettlauftest gegen MariaDB:Tests/Concurrency/concurrency-slots-race.php.- Bewusst akzeptierte Einschränkung (keine Behebung): Die DOM-Übersetzung nimmt gerenderte Segmente an, die keine Datensätze sind; ohne
domTranslationCacheOnlylässt sich daher beliebiger Text im Rahmen der Budgets übersetzen, und ein Aufrufer kann das globale Budget bis zum Ende des Zeitfensters für alle aufbrauchen.
Tests¶
Die Functional-Tests laufen auf SQLite mit Fake-Anbietern im Prozess — es wird nie eine kostenpflichtige API angesprochen. Tests/Concurrency/rate-limiter-race.php und Tests/Concurrency/concurrency-slots-race.php prüfen Zähler und Anfrage-Plätze mit echten Parallelprozessen gegen MariaDB/MySQL (siehe Dateiköpfe). Tests/JavaScript/dom-switch.cjs prüft die DOM-Übersetzung im Browser (Playwright, seit 1.4.4): Wechsel zwischen zwei Fremdsprachen im Cache-only-Betrieb bei Netzfehlern und mit einem Slider, der seine Folien neu einhängt — NODE_PATH=<pfad>/node_modules node Tests/JavaScript/dom-switch.cjs.
Barrierefreiheit¶
<html lang>unddirwerden bei jedem Umschalten gesetzt (WCAG 3.1.1 / 3.1.2)- Sprachnamen als Autonyme mit
lang-Attribut je Button/Link - Dropdown trägt
data-nt-notranslate— Sprachnamen werden beim Übersetzen der Seite nicht selbst übersetzt role="status"/aria-live="polite"für Fehlermeldungen im Navbar-Bereicharia-live="polite"auf dem Disclaimer-Banneraria-busy="true"auf<main>während der Übersetzung (Overlay-Spinner)aria-pressedauf Buttons für den aktiven Zustand; native Sprachen als Links mitaria-current- Native Sprachen als Links (semantisch korrekt, ohne JS nutzbar, kein WCAG-3.2.2-Problem)
- DOM-Sprachen und Vereinfachung als Buttons (kein Seitenaufruf, reines Client-Verhalten)
data-ntlingua-nativeauf nativen Links → localStorage-Cleanup vor der Navigation- Voll tastaturbedienbar, sichtbarer Fokus; Widget schließt bei Escape
Bekannte Eigenheiten¶
PHP: Numerische String-Keys in Arrays¶
PHP konvertiert Array-Keys, die wie Ganzzahlen aussehen ("1999", "42"), automatisch zu int. In TextTransformationService::translateStrings() kann das im foreach ($missing as $orig => $_)-Loop dazu führen, dass $orig ein int ist, obwohl normalize(string $s) einen string erwartet. Fix: $orig = (string)$orig; am Beginn des Loops.
DeepL: source_lang akzeptiert keine Locale-Varianten¶
DeepLs source_lang-Parameter akzeptiert nur Basiscodes (EN, DE), keine Locale-Varianten (EN-US, DE-AT). Das <html lang>-Attribut kann Locale-Varianten enthalten (z. B. en-US bei locale: en-US in der TYPO3-Site-Konfiguration). DeepLTranslator kürzt den Source-Code mit explode('-', $sourceLang)[0].
Chrome Autotranslate blockiert den TreeWalker¶
Chromes eingebauter Übersetzer setzt class="notranslate" auf das <html>-Element. Die ignored()-Funktion stoppt explizit bei document.documentElement und prüft das <html>-Element selbst nicht — andernfalls würde der gesamte Seiteninhalt als ausgeschlossen gewertet und der TreeWalker lieferte einen leeren Text-Pool.
Zwei Sprachwähler-Instanzen (Mobile + Desktop)¶
Das Partial wird zweimal gerendert (Mobile und Desktop, CSS-gesteuert). setToggleLabel() nutzt querySelectorAll('.nt-lingua-active-label'), um beide Toggle-Buttons synchron zu halten.