.. 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
============================
.. versionadded:: 3.1.0
Comprehensive localization infrastructure with 23 languages and Crowdin integration.
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
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:: default
:name: textdb-viewhelper-default
:type: string
:Required: false
:Default: ``null``
Fallback text displayed if the translation is not found in the database.
**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:**
.. php:method:: getTranslation(string $component, string $type, string $placeholder, int $languageUid = 0)
Retrieves a translation record from the database.
:param string $component: Component identifier
:param string $type: Type identifier
:param string $placeholder: Translation key
:param int $languageUid: Language UID (0 for default language)
:returns: Translation domain model or null if not found
:returntype: ``\\Netresearch\\NrTextdb\\Domain\\Model\\Translation|null``
**Example:**
.. code-block:: php
$translation = $this->translationService->getTranslation(
component: 'website',
type: 'label',
placeholder: 'welcome.message',
languageUid: 1
);
if ($translation) {
echo $translation->getValue();
}
createTranslation()
~~~~~~~~~~~~~~~~~~~
.. code-block:: php
public function createTranslation(
string $component,
string $type,
string $placeholder,
string $value,
int $languageUid = 0
): Translation
Create new translation record.
**Example:**
.. code-block:: php
$translation = $this->translationService->createTranslation(
component: 'shop',
type: 'label',
placeholder: 'cart.add',
value: 'Add to cart',
languageUid: 0
);
ImportService
-------------
Service for importing XLIFF files.
**Injection:**
.. code-block:: php
use Netresearch\NrTextdb\Service\ImportService;
public function __construct(
private readonly ImportService $importService
) {}
**Methods:**
importXliffFile()
~~~~~~~~~~~~~~~~~
.. code-block:: php
public function importXliffFile(
string $filePath,
bool $overwriteExisting = false
): array
Import translations from XLIFF file.
**Returns:** Array with import statistics
.. code-block:: php
[
'imported' => 150,
'updated' => 25,
'skipped' => 10,
'errors' => []
]
**Example:**
.. code-block:: php
$result = $this->importService->importXliffFile(
filePath: '/path/to/translations.xlf',
overwriteExisting: true
);
echo "Imported: {$result['imported']} translations";
.. _dev-repositories:
Repositories
============
TranslationRepository
---------------------
Repository for translation records.
**Custom Methods:**
findByComponentAndType()
~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: php
public function findByComponentAndType(
Component $component,
Type $type
): QueryResultInterface
Find all translations for component and type.
findByPlaceholder()
~~~~~~~~~~~~~~~~~~~
.. code-block:: php
public function findByPlaceholder(
string $placeholder,
int $languageUid = 0
): ?Translation
Find translation by placeholder key.
**Example:**
.. code-block:: php
use Netresearch\NrTextdb\Domain\Repository\TranslationRepository;
public function __construct(
private readonly TranslationRepository $repository
) {}
public function myAction(): void
{
$translations = $this->repository->findByComponentAndType(
$component,
$type
);
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 AbstractEntity
{
protected string $name = '';
protected string $identifier = '';
}
Type Model
----------
.. code-block:: php
class Type extends AbstractEntity
{
protected string $name = '';
protected string $identifier = '';
}
Environment Model
-----------------
.. code-block:: php
class Environment extends AbstractEntity
{
protected string $name = '';
protected string $identifier = '';
}
.. _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 [file-path]
**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) {
$result = $this->importService->importXliffFile($file);
$output->writeln("Imported {$result['imported']} 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->findByIdentifier('website');
$type = $this->typeRepository->findByIdentifier('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(string $component): string
{
$translations = $this->repository->findByComponent($component);
$data = [];
foreach ($translations as $translation) {
$data[$translation->getPlaceholder()] = $translation->getValue();
}
return json_encode($data, JSON_PRETTY_PRINT);
}
public function exportToCsv(string $component): string
{
$translations = $this->repository->findByComponent($component);
$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) {
$translation = $this->translationService->getTranslation(
component: 'menu',
type: 'label',
placeholder: $item['key'],
languageUid: $languageUid
);
$item['title'] = $translation?->getValue() ?? $item['title'];
}
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 getTranslation(
string $component,
string $type,
string $placeholder,
int $languageUid = 0
): ?Translation {
// Add custom caching
$cacheKey = "{$component}_{$type}_{$placeholder}_{$languageUid}";
if ($cached = $this->cache->get($cacheKey)) {
return $cached;
}
$translation = parent::getTranslation(
$component,
$type,
$placeholder,
$languageUid
);
$this->cache->set($cacheKey, $translation);
return $translation;
}
}
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'];
$translation = $this->translationService->getTranslation(
component: $component,
type: 'label',
placeholder: $placeholder
);
return $translation?->getValue() ?? $this->arguments['key'];
}
}
.. _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
$translation = $this->translationService->getTranslation(
component: 'website',
type: 'label',
placeholder: 'test.key',
languageUid: 0
);
if (!$translation) {
// Check: Component exists?
// Check: Type exists?
// Check: Record in correct storage folder?
// Check: createIfMissing enabled?
}
.. _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/