Developer Manual
Overview
This section covers integration of the TextDB extension into your TYPO3 project, including ViewHelpers, APIs, services, and extension points.
Architecture
Domain Model
The extension uses Extbase domain-driven design:
Domain Models:
├── Translation # Main translation record
├── Component # Logical grouping (website, shop, etc.)
├── Type # Category (label, message, error)
└── Environment # Context (dev, staging, production)
Repositories:
├── TranslationRepository
├── ComponentRepository
├── TypeRepository
└── EnvironmentRepository
Services:
├── TranslationService # Core translation logic
└── ImportService # XLIFF import handling
Controllers:
└── TranslationController # Backend module
Dependency Injection
All services use constructor injection via Configuration/:
services:
_defaults:
autowire: true
autoconfigure: true
public: false
Netresearch\NrTextdb\:
resource: '../Classes/*'
exclude: '../Classes/Domain/Model/*'
Localization Infrastructure
The extension includes a robust localization infrastructure supporting 23 languages for the backend interface.
Supported Languages
European (13): Afrikaans (af), Czech (cs), Danish (da), German (de), Spanish (es), Finnish (fi), French (fr), Italian (it), Dutch (nl), Norwegian (no), Polish (pl), Portuguese (pt), Swedish (sv)
Asian & African (10): Arabic (ar), Hindi (hi), Indonesian (id), Japanese (ja), Korean (ko), Russian (ru), Swahili (sw), Thai (th), Vietnamese (vi), Chinese (zh)
Technical Implementation
XLIFF 1.2 Standard
All translation files follow XLIFF 1.2 specification:
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="EXT:nr_textdb/Resources/Private/Language/locallang.xlf" date="..." product-name="nr_textdb">
<header/>
<body>
<trans-unit id="module.title" resname="module.title" translate="no">
<source>Netresearch</source>
</trans-unit>
</body>
</file>
</xliff>
Proper Names Protection
Brand names are marked as untranslatable using translate="no" attribute:
<trans-unit id="module.title" resname="module.title" translate="no">
<source>Netresearch</source>
</trans-unit>
This ensures "Netresearch" and "TextDb" remain unchanged across all translations.
UTF-8 Encoding
All language files use UTF-8 encoding to support non-Latin scripts:
- Right-to-left scripts: Arabic (العربية)
- Asian ideographs: Chinese (中文), Japanese (日本語), Korean (한국어)
- Indic scripts: Hindi (हिन्दी), Thai (ไทย)
File Structure
Each language has 5 translation files:
Resources/Private/Language/
├── {lang}.locallang.xlf # General interface labels
├── {lang}.locallang_db.xlf # Database field labels
├── {lang}.locallang_mod.xlf # Backend module labels
├── {lang}.locallang_mod_sync.xlf # Sync module labels
└── {lang}.locallang_mod_textdb.xlf # TextDB module labels
Total: 116 XLIFF files (23 languages × 5 files + 1 source file per type)
Community Translation Workflow
The extension integrates with TYPO3's centralized Crowdin translation system:
Configuration (crowdin.):
files:
- source: Resources/Private/Language/locallang.xlf
translation: Resources/Private/Language/%two_letters_code%.locallang.xlf
- source: Resources/Private/Language/locallang_db.xlf
translation: Resources/Private/Language/%two_letters_code%.locallang_db.xlf
# ... additional file types
Translation Process:
- Translators contribute via https://crowdin.com/project/typo3-cms
- TYPO3 translation coordinators review submissions
- Approved translations automatically sync to repository
- Changes included in next extension release
Adding New Languages:
To add a new language:
- Create language files following naming convention:
{lang}.locallang*. xlf - Copy structure from English source files
- Update
crowdin.with new language patternsyml - Submit to Crowdin for community translation
See CONTRIBUTING.md for detailed translation contribution guidelines.
ViewHelpers
TextDB ViewHelper
-
class
TextdbViewHelper -
Main ViewHelper for displaying translations in Fluid templates.
- Namespace
-
Netresearch\Nr Textdb\ View Helpers - Extends
-
TYPO3Fluid\Fluid\ Core\ View Helper\ Abstract View Helper
Usage:
{namespace textdb=Netresearch\NrTextdb\ViewHelpers} <textdb:textdb component="website" type="label" placeholder="welcome.message" />Copied!Parameters:
-
- Type
- string
- Required
- true
Component identifier for organizing translations (e.g., "website", "shop", "checkout").
-
- Type
- string
- Required
- true
- Default
P
Type identifier categorizing the translation (e.g., "label", "message", "error", "button").
-
- Type
- string
- Required
- true
Unique translation key within the component and type context.
-
- Type
- string
- Required
- false
- Default
default
Environment name for contextual translations.
Output:
Returns the translated text for the current page language. When that language has no usable entry, the ViewHelper falls back as described in Lookup Order and Language Fallback.
Example:
<!-- Simple usage -->
<h1><textdb:textdb component="website" type="label" placeholder="page.title" /></h1>
<!-- With explicit environment -->
<p>
<textdb:textdb
component="website"
type="message"
placeholder="welcome.text"
environment="production"
/>
</p>
<!-- Inline syntax -->
{textdb:textdb(component: 'website', type: 'label', placeholder: 'button.submit')}
Translate ViewHelper
Alternative ViewHelper compatible with f:translate interface.
Usage:
{namespace textdb=Netresearch\NrTextdb\ViewHelpers}
<textdb:translate key="LLL:EXT:my_ext:path.to.key" />
Component Configuration:
Set component in controller:
use Netresearch\NrTextdb\ViewHelpers\TranslateViewHelper;
class MyController extends ActionController
{
public function initializeAction(): void
{
TranslateViewHelper::$component = 'my-component';
}
}
Auto-Import Feature:
When enabled (create), this ViewHelper will:
- Load translation from XLIFF file on first request
- Create TextDB record automatically
- Use TextDB record on subsequent requests
Lookup Order and Language Fallback
Both ViewHelpers look for a value in this order and render the first one they find:
- The TextDB entry in the current language. A hidden entry, an entry with
an empty value, or one that was auto-created and never edited, does not
count. A value that equals its key (
Login→Login) does count. textdb:only: the label from the language file (XLIFF). This is the label in the current language or, when the file has no translation for the key, the file's source label. Step 3 therefore runs fortranslate textdb:only when the key is in no language file, or its label is empty.translate - The TextDB entries in the languages of the site language's fallback
chain (
fallbackin the site configuration), in their order, then the entry in the default language (Language Ids 0). The same rules as in step 1 apply to each entry. - The key itself.
TYPO3 passes the fallback on for every fallback,
so step 3 follows them for strict, fallback and free alike. A
site language without fallback languages still falls back to the default
language in step 3.
When create is enabled, step 1 creates the missing entry in
the current language, as before. Steps 3 and 4 never create translation
records.
A hidden entry renders like a missing one: in step 1 the lookup goes on
with step 2, and in step 3 the hidden entry is skipped. The lookup still
finds the hidden entry, so create does not create a second
entry for its key. The backend module lists, edits, exports and imports
hidden entries as before. Deleted entries are never found.
For example, a German page (language 1) whose site language falls back
to 0 renders the English TextDB entry when no German entry exists. With
textdb:, a label in the language file wins over that English
entry, whether it is the German label or the file's source label.
Services API
TranslationService
-
class
TranslationService -
Core service for translation management and retrieval.
- Namespace
-
Netresearch\Nr Textdb\ Service
Dependency Injection:
use Netresearch\NrTextdb\Service\TranslationService; class MyClass { public function __construct( private readonly TranslationService $translationService ) {} }Copied!Methods:
translate()
public function translate( string $placeholder, string $typeName, string $componentName, string $environmentName, ): stringCopied!Retrieves a translated string from the database. If
createis enabled and the translation doesn't exist, it will be auto-created with a placeholder value.If Missing - param string $placeholder
-
Translation key
- param string $typeName
-
Type name (e.g., "label", "button")
- param string $componentName
-
Component name (e.g., "website", "checkout")
- param string $environmentName
-
Environment name (e.g., "default")
- returns
-
The translated value, or the placeholder if not found
Example:
$value = $this->translationService->translate( placeholder: 'welcome.message', typeName: 'label', componentName: 'website', environmentName: 'default', );Copied!translatelooks up the current language only. It returns the key both when there is no usable entry and when the value equals the key; use() findto tell the two apart.Translation () findTranslation()
public function findTranslation( string $placeholder, string $typeName, string $componentName, string $environmentName, ): ?stringCopied!Same lookup as
translate, including the auto-creation with() create, but returnsIf Missing nullwhen the current language has no usable entry: none, a hidden one, an empty value, or an auto-created one. The ViewHelpers use it for step 1 of Lookup Order and Language Fallback.- returns
-
The value, or
nullif the current language has no usable entry
translateFromFallbackLanguages()
public function translateFromFallbackLanguages( string $placeholder, string $typeName, string $componentName, string $environmentName, ): ?stringCopied!Returns the value of the first usable entry along the fallback chain of the current language, then in the default language, as in step 3 of Lookup Order and Language Fallback. The current language itself is not searched. The method never creates translation records.
- param string $placeholder
-
Translation key
- param string $typeName
-
Type name (e.g., "label", "button")
- param string $componentName
-
Component name (e.g., "website", "checkout")
- param string $environmentName
-
Environment name (e.g., "default")
- returns
-
The value, or
nullif no fallback language has one
Example:
$value = $this->translationService->findTranslation('welcome.message', 'label', 'website', 'default') ?? $this->translationService->translateFromFallbackLanguages('welcome.message', 'label', 'website', 'default') ?? 'welcome.message';Copied!createTranslation()
public function createTranslation( Environment $environment, Component $component, Type $type, string $placeholder, int $sysLanguageUid = 0, string $value = '', ): TranslationCopied!Creates a new translation record in the database.
Example:
$translation = $this->translationService->createTranslation( environment: $environment, component: $component, type: $type, placeholder: 'cart.add', sysLanguageUid: 0, value: 'Add to cart', );Copied!getAllLanguages()
public function getAllLanguages(): arrayCopied!Returns all configured site languages from the first available site.
- returns
-
Array of
SiteobjectsLanguage
ImportService
Service for importing XLIFF files.
Injection:
use Netresearch\NrTextdb\Service\ImportService;
public function __construct(
private readonly ImportService $importService
) {}
Methods:
The counts, the error messages and the keys of skipped units collect on an
Import that the caller creates and passes in. One result can go
through several calls; read the totals at the end.
importFile()
public function importFile(
string $file,
bool $forceUpdate,
ImportResult $result,
): void
Imports an XLIFF file. The language comes from the file name: de.
is German, a name without a language prefix is the default language.
$force overwrites existing records. The method reads the file with
load and writes it with import.
It throws when the file cannot be read, when a key lacks a component, type or placeholder, and when a write fails. A file that fails imports nothing.
Example:
use Netresearch\NrTextdb\Service\ImportResult;
$result = new ImportResult();
$this->importService->importFile('/path/to/de.textdb_website.xlf', true, $result);
echo sprintf('Imported: %d, Updated: %d', $result->getImported(), $result->getUpdated());
foreach ($result->getErrors() as $error) {
echo $error, PHP_EOL;
}
loadFile()
public function loadFile(string $file, string $locale): LoadedXliffFile
Reads an XLIFF file with TYPO3's Xliff and returns a
Loaded with three public properties:
entries: the messages, keyedcomponent|type |placeholder unapproved: keys of the units left out because they are not approvedKeys partially: keys of the plural units kept without their forms that are not approved. TYPO3 keeps the first two forms of a plural unit only, so an unapproved third or later form does not show here.Approved Keys
While $GLOBALS
is enabled, the file is read a second time with the setting disabled to find
these keys; the setting is restored afterwards. While it is disabled, both
lists are empty. Throws Invalid or
Invalid when the file cannot be read.
importEntries()
public function importEntries(
array $entries,
int $languageUid,
bool $forceUpdate,
ImportResult $result,
array $unapprovedKeys = [],
array $partiallyApprovedKeys = [],
): void
Imports loaded messages for one language in one database transaction. Every
key is checked before the first write. A value that is empty or only
whitespace is skipped. A key in $partially is recorded only
when its entry is written, that is imported or updated; an entry without text
is recorded as such, and an existing entry that is kept is not recorded. The
counts and the skipped keys reach $result once
the transaction has committed; after a rollback only the error messages do,
and the exception is thrown on.
The translations are written without Extbase, in chunks of 1,000 entries:
new records with one bulk INSERT, a changed record with one UPDATE,
and the reference index of each written record through TYPO3's
Reference. The rows are the ones Extbase wrote before. The Extbase
persistence events Entity,
Entity, Entity and
Entity no longer fire for imported
translations; they still fire for an environment, component or type the
import creates. The error rule of import applies: a database
error, and any exception raised while such a record is written, rolls back
the file.
importEntry()
public function importEntry(
int $languageUid,
?string $componentName,
?string $typeName,
string $placeholder,
string $value,
bool $forceUpdate,
ImportResult $result,
): bool
Imports a single translation entry through Extbase and returns whether it
imported or updated it. The caller persists the record, for example with
Persistence; import does not
use this method. When the entry fails, it records the error on $result
and returns false. Two kinds of error are thrown instead, so that a transaction around
the import rolls back on every database: a database error, as a DBAL
exception or the Extbase Sql that wraps one, and any
exception raised while the persistence session is written. The lookup of a
component, type or environment that does not exist yet writes the session,
and with it the pending records of earlier entries.
ImportResult
Read the totals with get, get and get,
and the skipped units with get, get
and get. Each returns the keys once. add adds
another result to this one.
Repositories
TranslationRepository
Repository for translation records.
Custom Methods:
findAllByComponentTypePlaceholderValueAndLanguage()
public function findAllByComponentTypePlaceholderValueAndLanguage(
int $component = 0,
int $type = 0,
?string $placeholder = null,
?string $value = null,
int $languageId = 0,
): QueryResultInterface
Find all translations filtered by component UID, type UID, placeholder substring, value substring, and/or language ID. All parameters are optional filters.
findByEnvironmentComponentTypePlaceholderAndLanguage()
public function findByEnvironmentComponentTypePlaceholderAndLanguage(
Environment $environment,
Component $component,
Type $type,
string $placeholder,
int $languageUid,
): ?Translation
Find a single translation by exact environment, component, type, placeholder, and language.
Hidden translations are returned too; callers that render a value check
Translation:: themselves.
findByEnvironmentComponentTypeAndPlaceholder()
public function findByEnvironmentComponentTypeAndPlaceholder(
Environment $environment,
Component $component,
Type $type,
string $placeholder,
): ?Translation
Find the default language (sys_) translation for the given criteria.
Example:
use Netresearch\NrTextdb\Domain\Repository\TranslationRepository;
public function __construct(
private readonly TranslationRepository $repository
) {}
public function myAction(): void
{
$translations = $this->repository
->findAllByComponentTypePlaceholderValueAndLanguage(
component: $componentUid,
type: $typeUid,
);
foreach ($translations as $translation) {
// Process translations
}
}
Domain Models
Translation Model
Main translation entity.
Properties:
class Translation extends AbstractEntity
{
protected string $placeholder = '';
protected string $value = '';
protected ?Component $component = null;
protected ?Type $type = null;
protected ?Environment $environment = null;
protected int $sysLanguageUid = 0;
}
Getters/Setters:
$translation = new Translation();
$translation->setPlaceholder('welcome.message');
$translation->setValue('Welcome!');
$translation->setComponent($component);
$translation->setType($type);
echo $translation->getValue(); // "Welcome!"
Component Model
class Component extends AbstractValueObject
{
protected string $name = '';
}
Type Model
class Type extends AbstractValueObject
{
protected string $name = '';
}
Environment Model
class Environment extends AbstractValueObject
{
protected string $name = '';
}
Console Commands
ImportCommand
CLI command for importing translations.
Location: Classes/
Usage:
vendor/bin/typo3 nr_textdb:import [extensionKey] [--override|-o]
Configuration:
# Configuration/Services.yaml
Netresearch\NrTextdb\Command\ImportCommand:
tags:
- name: 'console.command'
command: 'nr_textdb:import'
description: 'Imports textdb records from language files'
schedulable: false
Creating Custom Commands:
namespace MyVendor\MyExt\Command;
use Netresearch\NrTextdb\Service\ImportResult;
use Netresearch\NrTextdb\Service\ImportService;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class CustomImportCommand extends Command
{
public function __construct(
private readonly ImportService $importService
) {
parent::__construct();
}
protected function execute(
InputInterface $input,
OutputInterface $output
): int {
$files = glob('/path/to/translations/*.xlf');
foreach ($files as $file) {
$result = new ImportResult();
try {
$this->importService->importFile($file, false, $result);
} catch (\Throwable $exception) {
$output->writeln('<error>' . $exception->getMessage() . '</error>');
}
$output->writeln(sprintf(
'Imported: %d, Updated: %d from %s',
$result->getImported(),
$result->getUpdated(),
$file,
));
}
return Command::SUCCESS;
}
}
Events & Hooks
The extension currently uses standard Extbase/TYPO3 patterns. Future versions may add PSR-14 events for extensibility.
Potential Event Points:
- Before/After translation import
- Before/After translation creation
- Translation retrieval (for caching)
- Export generation
API Examples
Example 1: Programmatic Translation Management
use Netresearch\NrTextdb\Domain\Model\Translation;
use Netresearch\NrTextdb\Domain\Repository\TranslationRepository;
use Netresearch\NrTextdb\Domain\Repository\ComponentRepository;
use Netresearch\NrTextdb\Domain\Repository\TypeRepository;
use TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager;
class TranslationManager
{
public function __construct(
private readonly TranslationRepository $translationRepository,
private readonly ComponentRepository $componentRepository,
private readonly TypeRepository $typeRepository,
private readonly PersistenceManager $persistenceManager
) {}
public function createBulkTranslations(array $data): void
{
$component = $this->componentRepository->findByName('website');
$type = $this->typeRepository->findByName('label');
foreach ($data as $key => $value) {
$translation = new Translation();
$translation->setPlaceholder($key);
$translation->setValue($value);
$translation->setComponent($component);
$translation->setType($type);
$this->translationRepository->add($translation);
}
$this->persistenceManager->persistAll();
}
}
Example 2: Custom Export Functionality
use Netresearch\NrTextdb\Domain\Repository\TranslationRepository;
class CustomExporter
{
public function __construct(
private readonly TranslationRepository $repository
) {}
public function exportToJson(int $componentUid): string
{
$translations = $this->repository->findAllByComponentTypePlaceholderValueAndLanguage(component: $componentUid);
$data = [];
foreach ($translations as $translation) {
$data[$translation->getPlaceholder()] = $translation->getValue();
}
return json_encode($data, JSON_PRETTY_PRINT);
}
public function exportToCsv(int $componentUid): string
{
$translations = $this->repository->findAllByComponentTypePlaceholderValueAndLanguage(component: $componentUid);
$csv = "Placeholder,Value,Language\n";
foreach ($translations as $translation) {
$csv .= sprintf(
"%s,%s,%d\n",
$translation->getPlaceholder(),
$translation->getValue(),
$translation->getSysLanguageUid()
);
}
return $csv;
}
}
Example 3: Frontend Integration
use Netresearch\NrTextdb\Service\TranslationService;
use TYPO3\CMS\Core\Context\Context;
class FrontendTranslations
{
public function __construct(
private readonly TranslationService $translationService,
private readonly Context $context
) {}
public function getTranslatedMenu(array $menuItems): array
{
$languageUid = $this->context->getPropertyFromAspect(
'language',
'id'
);
foreach ($menuItems as &$item) {
$item['title'] = $this->translationService->translate(
placeholder: $item['key'],
typeName: 'label',
componentName: 'menu',
environmentName: 'default',
);
}
return $menuItems;
}
}
Testing
Unit Testing
Example unit test for Translation model:
namespace Netresearch\NrTextdb\Tests\Unit\Domain\Model;
use Netresearch\NrTextdb\Domain\Model\Translation;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
#[CoversClass(Translation::class)]
final class TranslationTest extends UnitTestCase
{
private Translation $subject;
protected function setUp(): void
{
parent::setUp();
$this->subject = new Translation();
}
#[Test]
public function setValueSetsValue(): void
{
$value = 'Test translation';
$this->subject->setValue($value);
self::assertSame($value, $this->subject->getValue());
}
}
Functional Testing
Example functional test for repository:
namespace Netresearch\NrTextdb\Tests\Functional\Domain\Repository;
use Netresearch\NrTextdb\Domain\Repository\TranslationRepository;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
final class TranslationRepositoryTest extends FunctionalTestCase
{
protected array $testExtensionsToLoad = [
'typo3conf/ext/nr_textdb',
];
private TranslationRepository $subject;
protected function setUp(): void
{
parent::setUp();
$this->subject = $this->get(TranslationRepository::class);
$this->importCSVDataSet(__DIR__ . '/Fixtures/translations.csv');
}
public function testFindAllReturnsAllTranslations(): void
{
$result = $this->subject->findAll();
self::assertCount(10, $result);
}
}
Extension Points
Extending the TranslationService
namespace MyVendor\MyExt\Service;
use Netresearch\NrTextdb\Service\TranslationService;
class ExtendedTranslationService extends TranslationService
{
public function translate(
string $placeholder,
string $typeName,
string $componentName,
string $environmentName,
): string {
// Add custom caching
$cacheKey = "{$componentName}_{$typeName}_{$placeholder}_{$environmentName}";
if ($cached = $this->cache->get($cacheKey)) {
return $cached;
}
$result = parent::translate(
$placeholder,
$typeName,
$componentName,
$environmentName,
);
$this->cache->set($cacheKey, $result);
return $result;
}
}
Custom ViewHelper
namespace MyVendor\MyExt\ViewHelpers;
use Netresearch\NrTextdb\Service\TranslationService;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
class CustomTranslateViewHelper extends AbstractViewHelper
{
public function __construct(
private readonly TranslationService $translationService
) {}
public function initializeArguments(): void
{
$this->registerArgument('key', 'string', 'Translation key', true);
}
public function render(): string
{
$parts = explode('.', $this->arguments['key']);
$component = $parts[0] ?? 'default';
$placeholder = $parts[1] ?? $this->arguments['key'];
return $this->translationService->translate(
placeholder: $placeholder,
typeName: 'label',
componentName: $component,
environmentName: 'default',
);
}
}
Best Practices
Naming Conventions
- Components:
-
- Use lowercase with hyphens:
website,shop-,cart user-portal - Be descriptive and consistent
- Use lowercase with hyphens:
- Types:
-
- Standard types:
label,message,error,notification - Use singular form:
buttonnotbuttons
- Standard types:
- Placeholders:
-
- Use dot notation:
page.,title button.,submit error.validation. email - Be hierarchical and descriptive
- Use dot notation:
Performance
- Cache translation lookups in frontend
- Use repository methods instead of manual queries
- Batch import/export operations for large datasets
- Consider Redis/Memcached for high-traffic sites
Code Quality
- Use strict types:
declare(strict_ types=1) - Type-hint all parameters and return values
- Write unit tests for business logic
- Document public APIs with PHPDoc
Security
- Escape translation output in templates:
{translation -> f:format. htmlspecialchars ()} - Validate XLIFF files before import
- Use prepared statements (Extbase does this automatically)
- Restrict file upload permissions
Developer Troubleshooting
ViewHelper Not Working
// Check namespace registration
{namespace textdb=Netresearch\NrTextdb\ViewHelpers}
// Verify storage PID configuration
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_textdb']['textDbPid']
Service Injection Failing
# Ensure Services.yaml is configured
services:
_defaults:
autowire: true
MyVendor\MyExt\MyClass:
public: true # If accessed via GeneralUtility::makeInstance
Translation Not Found
// Debug translation lookup
$value = $this->translationService->translate(
placeholder: 'test.key',
typeName: 'label',
componentName: 'website',
environmentName: 'default',
);
// If $value equals the placeholder, the translation was not found.
// Check:
// - Component 'website' exists in the configured storage folder?
// - Type 'label' exists?
// - Environment 'default' exists?
// - createIfMissing enabled in extension configuration?
Resources
- GitHub Repository: https://github.com/netresearch/t3x-nr-textdb
- TYPO3 Extension Repository: https://extensions.typo3.org/extension/nr_textdb
- TYPO3 Core API: https://docs.typo3.org/m/typo3/reference-coreapi/
- Extbase Documentation: https://docs.typo3.org/m/typo3/book-extbasefluid/
- Fluid ViewHelper Reference: https://docs.typo3.org/other/typo3/view-helper-reference/