Skip to content

Developer Guide

Architecture

Frontend-Request
    ├─ POST /nt-lingua/transform ─→ TransformApiMiddleware
    │        │
    │        ├─ isLanguageTarget(target) = true  ─→ TextTransformationService::translateStrings()
    │        └─ isLanguageTarget(target) = false ─→ TextTransformationService::transformField()
    └─ GET page request (cookie/param)
             └─ TransformProcessor (DataProcessing) ─→ transformField() [server-side fallback]

TextTransformationService
    ├─ einfache / leichte  ─→ AiProviderInterface (nt_ai)    + Prompts
    └─ language codes       ─→ TranslatorInterface (LLM/DeepL) + Glossary

TextCacheRepository
    └─ tx_ntlingua_text_cache (UNIQUE: content_hash + target)

Two coupling points:

  • Frontend → Server: POST /nt-lingua/transform handled by TransformApiMiddleware
  • Server → AI/Translation: TranslatorInterface (LLM or DeepL) + AiProviderInterface (nt-ai)

nt_lingua does not reference any concrete AI provider — the only external coupling is Netthinks\NtAi\Provider\AiProviderInterface.

Data model

tx_ntlingua_text_cache

Column Type Description
content_hash varchar(64) sha256(normalize(source) \| target \| model)
target varchar(20) einfache, leichte or language code (e.g. en)
source_lang varchar(10) Source language (default: de)
source_text mediumtext Original text
result_text mediumtext Transformed/translated text
provider varchar(30) AI provider or deepl
model varchar(80) Model used
tokens_used int Tokens consumed
readability decimal(5,2) Avg. words per sentence (lower = simpler)
source_uid int UID of the source record
source_table varchar(80) Table of the source record
source_field varchar(80) Field of the source record
crdate / tstamp int Unix timestamps

Unique key: (content_hash, target) — Einfache, Leichte and all language codes share the table without collision.

Cache invalidation: AfterDatabaseOperationsEventInvalidateTextCache deletes entries for the affected record when header or bodytext is saved.

tx_ntlingua_glossary

Column Description
source_term Source term (normalised before comparison)
target_lang Target language (ISO code) or * for all languages
target_term Fixed translation (leave empty for ignore mode)
mode fixed (force) or ignore (leave unchanged)

Glossary entries are evaluated before the cache and the translation provider. target_lang='*' applies language-independently to all DOM translations.

tt_content / pages extension fields

Field Description
tx_ntlingua_mt 1 = machine-translated (readonly in backend)
tx_ntlingua_srchash Hash of source fields at overlay creation time — enables delta detection

CLI reference

ntlingua:warmup

vendor/bin/typo3 ntlingua:warmup [--target=<target>] [--limit=<n>]
Option Default Description
--target einfache einfache, leichte, or an ISO language code
--limit 0 Max. records to process (0 = all)

Processes all tt_content records with sys_language_uid=0, deleted=0, hidden=0. Cache hits are skipped — the command is idempotent and safe to run as a nightly scheduler task.

# Pre-fill Einfache Sprache for all content elements:
vendor/bin/typo3 ntlingua:warmup --target=einfache

# Pre-fill English translation, at most 500 records:
vendor/bin/typo3 ntlingua:warmup --target=en --limit=500

ntlingua:overlays

vendor/bin/typo3 ntlingua:overlays --language=<uid> --code=<iso>
Option Description
--language sys_language_uid of the target language (must exist in TYPO3 site config)
--code ISO language code (e.g. en, fr)

Creates connected-mode l10n overlays. Only changed records are processed on repeated runs (delta detection via tx_ntlingua_srchash).

Translated fields:

Table Fields
pages title, nav_title, subtitle, seo_title, description
tt_content header, subheader, bodytext

Warning

Run ntlingua:overlays in the live workspace, not in draft workspaces.

JavaScript API

// Switch language or mode:
NtLingua.transform('fr', buttonElement);   // DOM translation
NtLingua.transform('einfache', btn);       // Plain language
NtLingua.transform('de');                  // Reset to source language

// Query current state:
NtLingua.getCurrent();   // e.g. 'de', 'fr', 'einfache'

Custom events

document.addEventListener('NtLingua:ready', (e) => {
    console.log('Initialised, active target:', e.detail.target);
});
document.addEventListener('NtLingua:done', (e) => {
    console.log('Switch complete:', e.detail.target);
});

Adding a custom translator

Implement TranslatorInterface and register it as an alias in Services.yaml:

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 of your extension:
Netthinks\NtLingua\Translator\TranslatorInterface:
  alias: MyVendor\MyExt\Translator\MyTranslator

Fluid wrappers (required for DOM field swap)

Content fields must be wrapped with data-ntlingua-* attributes in FSC template overrides:

<!-- bodytext (e.g. in an 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>

Place Fluid overrides under packages/netthinks/Resources/Private/Extensions/fluid_styled_content/.

Accessibility

  • <html lang> and dir are updated on every switch (WCAG 3.1.1 / 3.1.2)
  • Language names rendered as autonyms with per-button lang attributes
  • The dropdown carries data-nt-notranslate — language names are not translated when the page itself is translated
  • role="status" / aria-live="polite" for error messages in the navbar area
  • aria-live="polite" on the disclaimer banner
  • aria-busy="true" on <main> during translation (overlay spinner)
  • aria-pressed on buttons for active state; native languages as links with aria-current
  • Native languages as links (semantically correct, usable without JS, no WCAG 3.2.2 issue)
  • DOM languages and Plain Language as buttons (no page load, pure client-side behaviour)
  • data-ntlingua-native on native links → localStorage cleanup before navigation
  • Fully keyboard-navigable, visible focus; widget closes on Escape

Known issues

PHP: numeric string keys in arrays

PHP automatically converts array keys that look like integers ("1999", "42") to int. In TextTransformationService::translateStrings() this can cause $orig to be int in the foreach ($missing as $orig => $_) loop, even though normalize(string $s) expects a string. Fix: add $orig = (string)$orig; at the top of the loop.

DeepL: source_lang does not accept locale variants

DeepL's source_lang parameter only accepts base codes (EN, DE), not locale variants (EN-US, DE-AT). The <html lang> attribute may contain a locale variant (e.g. en-US when locale: en-US is set in the TYPO3 site configuration). DeepLTranslator truncates the source code with explode('-', $sourceLang)[0].

Chrome Autotranslate blocks the TreeWalker

Chrome's built-in translator sets class="notranslate" on the <html> element. The ignored() function explicitly stops at document.documentElement and does not check the <html> element itself — otherwise the entire page content would be treated as excluded and the TreeWalker would return an empty text pool.

Two switcher instances (mobile + desktop)

The partial is rendered twice (mobile and desktop, controlled by CSS). setToggleLabel() uses querySelectorAll('.nt-lingua-active-label') to keep both toggle buttons in sync.