.. SPDX-License-Identifier: GPL-3.0-or-later
.. SPDX-FileCopyrightText: Netresearch DTT GmbH
.. include:: /Includes.rst.txt
.. _developer:
==================
Developer Manual
==================
.. _dev-overview:
Overview
========
This section covers integration of the TextDB extension into your TYPO3
project, including ViewHelpers, APIs, services, and extension points.
.. _dev-architecture:
Architecture
============
Domain Model
------------
The extension uses Extbase domain-driven design:
.. code-block:: none
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.yaml`:
.. code-block:: yaml
services:
_defaults:
autowire: true
autoconfigure: true
public: false
Netresearch\NrTextdb\:
resource: '../Classes/*'
exclude: '../Classes/Domain/Model/*'
.. _dev-localization:
Localization Infrastructure
============================
The extension includes a robust localization infrastructure supporting **31 languages** for the backend interface.
Supported Languages
-------------------
**European (19):**
Afrikaans (af), Catalan (ca), Czech (cs), Danish (da), German (de), Greek (el), Spanish (es), Finnish (fi), French (fr), Hungarian (hu), Italian (it), Dutch (nl), Norwegian (no), Polish (pl), Portuguese (pt), Romanian (ro), Serbian (sr), Swedish (sv), Ukrainian (uk)
**Asian & African (12):**
Arabic (ar), Hebrew (he), Hindi (hi), Indonesian (id), Japanese (ja), Korean (ko), Russian (ru), Swahili (sw), Thai (th), Turkish (tr), Vietnamese (vi), Chinese (zh)
Technical Implementation
------------------------
**XLIFF 1.2 Standard**
All translation files follow XLIFF 1.2 specification:
.. code-block:: xml
Netresearch
**Proper Names Protection**
Brand names are marked as untranslatable using ``translate="no"`` attribute:
.. code-block:: xml
Netresearch
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 4 translation files:
.. code-block:: none
Resources/Private/Language/
├── {lang}.locallang.xlf # General interface labels
├── {lang}.locallang_db.xlf # Database field labels
├── {lang}.locallang_mod_sync.xlf # Sync module labels
└── {lang}.locallang_mod_textdb.xlf # TextDB module labels
Total: 128 XLIFF files (31 languages × 4 files + 1 source file per type)
Community Translation Workflow
-------------------------------
The extension integrates with TYPO3's centralized Crowdin translation system:
**Configuration** (``crowdin.yml``):
.. code-block:: yaml
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:**
1. Translators contribute via https://crowdin.com/project/typo3-cms
2. TYPO3 translation coordinators review submissions
3. Approved translations automatically sync to repository
4. Changes included in next extension release
**Adding New Languages:**
To add a new language:
1. Create language files following naming convention: ``{lang}.locallang*.xlf``
2. Copy structure from English source files
3. Update ``crowdin.yml`` with new language patterns
4. Submit to Crowdin for community translation
See `CONTRIBUTING.md `_ for detailed translation contribution guidelines.
.. _dev-viewhelpers:
ViewHelpers
===========
TextDB ViewHelper
-----------------
.. php:class:: TextdbViewHelper
Main ViewHelper for displaying translations in Fluid templates.
:Namespace: ``Netresearch\NrTextdb\ViewHelpers``
:Extends: ``TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper``
**Usage:**
.. code-block:: html
{namespace textdb=Netresearch\NrTextdb\ViewHelpers}
**Parameters:**
.. confval:: component
:name: textdb-viewhelper-component
:type: string
:Required: true
Component identifier for organizing translations (e.g., "website", "shop", "checkout").
.. confval:: type
:name: textdb-viewhelper-type
:type: string
:Required: true
:Default: ``P``
Type identifier categorizing the translation (e.g., "label", "message", "error", "button").
.. confval:: placeholder
:name: textdb-viewhelper-placeholder
:type: string
:Required: true
Unique translation key within the component and type context.
.. confval:: environment
:name: textdb-viewhelper-environment
: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
:ref:`dev-viewhelpers-fallback`.
**Example:**
.. code-block:: html
{textdb:textdb(component: 'website', type: 'label', placeholder: 'button.submit')}
Translate ViewHelper
--------------------
Alternative ViewHelper compatible with f:translate interface.
**Usage:**
.. code-block:: html
{namespace textdb=Netresearch\NrTextdb\ViewHelpers}
**Component Configuration:**
Set component in controller:
.. code-block:: php
use Netresearch\NrTextdb\ViewHelpers\TranslateViewHelper;
class MyController extends ActionController
{
public function initializeAction(): void
{
TranslateViewHelper::$component = 'my-component';
}
}
**Auto-Import Feature:**
When enabled (`createIfMissing = 1`), this ViewHelper will:
1. Load translation from XLIFF file on first request
2. Create TextDB record automatically
3. Use TextDB record on subsequent requests
.. _dev-viewhelpers-fallback:
Lookup Order and Language Fallback
----------------------------------
Both ViewHelpers look for a value in this order and render the first one
they find:
1. 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.
2. ``textdb:translate`` 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
for ``textdb:translate`` only when the key is in no language file, or
its label is empty.
3. The TextDB entries in the languages of the site language's fallback
chain (``fallbackLanguageIds`` in the site configuration), in their
order, then the entry in the default language (``0``). The same rules as
in step 1 apply to each entry.
4. The key itself.
TYPO3 passes the ``fallbackLanguageIds`` on for every ``fallbackType``,
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 ``createIfMissing`` 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 ``createIfMissing`` 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:translate``, a label in the language file wins over that English
entry, whether it is the German label or the file's source label.
.. _dev-services:
Services API
============
TranslationService
------------------
.. php:class:: TranslationService
Core service for translation management and retrieval.
:Namespace: ``Netresearch\NrTextdb\Service``
**Dependency Injection:**
.. code-block:: php
use Netresearch\NrTextdb\Service\TranslationService;
class MyClass
{
public function __construct(
private readonly TranslationService $translationService
) {}
}
**Methods:**
translate()
~~~~~~~~~~~
.. code-block:: php
public function translate(
string $placeholder,
string $typeName,
string $componentName,
string $environmentName,
): string
Retrieves a translated string from the database. If ``createIfMissing`` is enabled
and the translation doesn't exist, it will be auto-created with a placeholder value.
: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:**
.. code-block:: php
$value = $this->translationService->translate(
placeholder: 'welcome.message',
typeName: 'label',
componentName: 'website',
environmentName: 'default',
);
``translate()`` looks up the current language only. It returns the key
both when there is no usable entry and when the value equals the key; use
``findTranslation()`` to tell the two apart.
findTranslation()
~~~~~~~~~~~~~~~~~
.. code-block:: php
public function findTranslation(
string $placeholder,
string $typeName,
string $componentName,
string $environmentName,
): ?string
Same lookup as ``translate()``, including the auto-creation with
``createIfMissing``, but returns ``null`` when 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 :ref:`dev-viewhelpers-fallback`.
:returns: The value, or ``null`` if the current language has no usable entry
translateFromFallbackLanguages()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: php
public function translateFromFallbackLanguages(
string $placeholder,
string $typeName,
string $componentName,
string $environmentName,
): ?string
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
:ref:`dev-viewhelpers-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 ``null`` if no fallback language has one
**Example:**
.. code-block:: php
$value = $this->translationService->findTranslation('welcome.message', 'label', 'website', 'default')
?? $this->translationService->translateFromFallbackLanguages('welcome.message', 'label', 'website', 'default')
?? 'welcome.message';
createTranslation()
~~~~~~~~~~~~~~~~~~~~
.. code-block:: php
public function createTranslation(
Environment $environment,
Component $component,
Type $type,
string $placeholder,
int $sysLanguageUid = 0,
string $value = '',
): Translation
Creates a new translation record in the database.
**Example:**
.. code-block:: php
$translation = $this->translationService->createTranslation(
environment: $environment,
component: $component,
type: $type,
placeholder: 'cart.add',
sysLanguageUid: 0,
value: 'Add to cart',
);
getAllLanguages()
~~~~~~~~~~~~~~~~~
.. code-block:: php
public function getAllLanguages(): array
Returns all configured site languages from the first available site.
:returns: Array of ``SiteLanguage`` objects
ImportService
-------------
Service for importing XLIFF files.
**Injection:**
.. code-block:: php
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
``ImportResult`` that the caller creates and passes in. One result can go
through several calls; read the totals at the end.
importFile()
~~~~~~~~~~~~
.. code-block:: php
public function importFile(
string $file,
bool $forceUpdate,
ImportResult $result,
): void
Imports an XLIFF file. The language comes from the file name: ``de.textdb_x.xlf``
is German, a name without a language prefix is the default language.
``$forceUpdate`` overwrites existing records. The method reads the file with
``loadFile()`` and writes it with ``importEntries()``.
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:**
.. code-block:: php
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()
~~~~~~~~~~
.. code-block:: php
public function loadFile(string $file, string $locale): LoadedXliffFile
Reads an XLIFF file with TYPO3's ``XliffLoader`` and returns a
``LoadedXliffFile`` with three public properties:
* ``entries``: the messages, keyed ``component|type|placeholder``
* ``unapprovedKeys``: keys of the units left out because they are not approved
* ``partiallyApprovedKeys``: 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.
While ``$GLOBALS['TYPO3_CONF_VARS']['LANG']['requireApprovedLocalizations']``
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 ``InvalidXmlFileException`` or
``InvalidResourceException`` when the file cannot be read.
importEntries()
~~~~~~~~~~~~~~~
.. code-block:: php
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 ``$partiallyApprovedKeys`` 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
``ReferenceIndex``. The rows are the ones Extbase wrote before. The Extbase
persistence events ``EntityAddedToPersistenceEvent``,
``EntityUpdatedInPersistenceEvent``, ``EntityPersistedEvent`` and
``EntityFinalizedAfterPersistenceEvent`` no longer fire for imported
translations; they still fire for an environment, component or type the
import creates. The error rule of ``importEntry()`` applies: a database
error, and any exception raised while such a record is written, rolls back
the file.
importEntry()
~~~~~~~~~~~~~
.. code-block:: php
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
``PersistenceManagerInterface::persistAll()``; ``importEntries()`` 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 ``SqlErrorException`` 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 ``getImported()``, ``getUpdated()`` and ``getErrors()``,
and the skipped units with ``getSkippedUnapproved()``, ``getSkippedEmpty()``
and ``getPartiallyApproved()``. Each returns the keys once. ``add()`` adds
another result to this one.
.. _dev-repositories:
Repositories
============
TranslationRepository
---------------------
Repository for translation records.
**Custom Methods:**
findAllByComponentTypePlaceholderValueAndLanguage()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: php
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()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: php
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::isHidden()`` themselves.
findByEnvironmentComponentTypeAndPlaceholder()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: php
public function findByEnvironmentComponentTypeAndPlaceholder(
Environment $environment,
Component $component,
Type $type,
string $placeholder,
): ?Translation
Find the default language (``sys_language_uid = 0``) translation for the given criteria.
**Example:**
.. code-block:: php
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
}
}
.. _dev-domain-models:
Domain Models
=============
Translation Model
-----------------
Main translation entity.
**Properties:**
.. code-block:: php
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:**
.. code-block:: php
$translation = new Translation();
$translation->setPlaceholder('welcome.message');
$translation->setValue('Welcome!');
$translation->setComponent($component);
$translation->setType($type);
echo $translation->getValue(); // "Welcome!"
Component Model
---------------
.. code-block:: php
class Component extends AbstractValueObject
{
protected string $name = '';
}
Type Model
----------
.. code-block:: php
class Type extends AbstractValueObject
{
protected string $name = '';
}
Environment Model
-----------------
.. code-block:: php
class Environment extends AbstractValueObject
{
protected string $name = '';
}
.. _dev-commands:
Console Commands
================
ImportCommand
-------------
CLI command for importing translations.
**Location:** `Classes/Command/ImportCommand.php`
**Usage:**
.. code-block:: bash
vendor/bin/typo3 nr_textdb:import [extensionKey] [--override|-o]
**Configuration:**
.. code-block:: yaml
# 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:**
.. code-block:: php
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('' . $exception->getMessage() . '');
}
$output->writeln(sprintf(
'Imported: %d, Updated: %d from %s',
$result->getImported(),
$result->getUpdated(),
$file,
));
}
return Command::SUCCESS;
}
}
.. _dev-events:
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
.. _dev-api-examples:
API Examples
============
Example 1: Programmatic Translation Management
-----------------------------------------------
.. code-block:: php
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
---------------------------------------
.. code-block:: php
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
--------------------------------
.. code-block:: php
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;
}
}
.. _dev-testing:
Testing
=======
Unit Testing
------------
Example unit test for Translation model:
.. code-block:: php
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:
.. code-block:: php
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);
}
}
.. _dev-extension-points:
Extension Points
================
Extending the TranslationService
---------------------------------
.. code-block:: php
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
-----------------
.. code-block:: php
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',
);
}
}
.. _dev-best-practices:
Best Practices
==============
Naming Conventions
------------------
**Components:**
* Use lowercase with hyphens: `website`, `shop-cart`, `user-portal`
* Be descriptive and consistent
**Types:**
* Standard types: `label`, `message`, `error`, `notification`
* Use singular form: `button` not `buttons`
**Placeholders:**
* Use dot notation: `page.title`, `button.submit`, `error.validation.email`
* Be hierarchical and descriptive
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
.. _dev-troubleshooting:
Developer Troubleshooting
==========================
ViewHelper Not Working
-----------------------
.. code-block:: php
// Check namespace registration
{namespace textdb=Netresearch\NrTextdb\ViewHelpers}
// Verify storage PID configuration
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_textdb']['textDbPid']
Service Injection Failing
--------------------------
.. code-block:: yaml
# Ensure Services.yaml is configured
services:
_defaults:
autowire: true
MyVendor\MyExt\MyClass:
public: true # If accessed via GeneralUtility::makeInstance
Translation Not Found
---------------------
.. code-block:: php
// 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?
.. _dev-resources:
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/