.. 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 **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:
.. 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 5 translation files:
.. code-block:: none
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.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.
**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-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',
);
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:**
importFile()
~~~~~~~~~~~~~
.. code-block:: php
public function importFile(
string $file,
bool $forceUpdate,
int &$imported,
int &$updated,
array &$errors,
): void
Imports translations from a XLIFF file. Counters and errors are passed by reference.
**Example:**
.. code-block:: php
$imported = 0;
$updated = 0;
$errors = [];
$this->importService->importFile(
file: '/path/to/translations.xlf',
forceUpdate: true,
imported: $imported,
updated: $updated,
errors: $errors,
);
echo "Imported: {$imported}, Updated: {$updated}";
importEntry()
~~~~~~~~~~~~~~
.. code-block:: php
public function importEntry(
int $languageUid,
?string $componentName,
?string $typeName,
string $placeholder,
string $value,
bool $forceUpdate,
int &$imported,
int &$updated,
array &$errors,
): void
Imports a single translation entry into the database.
.. _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.
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\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) {
$imported = 0;
$updated = 0;
$errors = [];
$this->importService->importFile($file, false, $imported, $updated, $errors);
$output->writeln("Imported: {$imported}, Updated: {$updated} from {$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/