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/transformhandled byTransformApiMiddleware - 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: AfterDatabaseOperationsEvent → InvalidateTextCache 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¶
| 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¶
| 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>anddirare updated on every switch (WCAG 3.1.1 / 3.1.2)- Language names rendered as autonyms with per-button
langattributes - 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 areaaria-live="polite"on the disclaimer banneraria-busy="true"on<main>during translation (overlay spinner)aria-pressedon buttons for active state; native languages as links witharia-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-nativeon 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.