---
title: "Developer Manual"
manual: "Netresearch TextDB"
version: "main"
source: "Developer/Index.rst"
rendered: "2026-09-30T23:05:19+00:00"
---

# Developer Manual {#developer}

## Overview {#dev-overview}

This section covers integration of the TextDB extension into your TYPO3
project, including ViewHelpers, APIs, services, and extension points.

## Architecture {#dev-architecture}

### Domain Model {#domain-model}

The extension uses Extbase domain-driven design:

```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 {#dependency-injection}

All services use constructor injection via `Configuration/Services.yaml`:

```yaml
services:
    _defaults:
        autowire: true
        autoconfigure: true
        public: false

    Netresearch\NrTextdb\:
        resource: '../Classes/*'
        exclude: '../Classes/Domain/Model/*'
```

## Localization Infrastructure {#dev-localization}

The extension includes a robust localization infrastructure supporting **23 languages** for the backend interface.

### Supported Languages {#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 {#technical-implementation}

**XLIFF 1.2 Standard**

All translation files follow XLIFF 1.2 specification:

```xml
<?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:

```xml
<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 {#file-structure}

Each language has 5 translation files:

```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 {#community-translation-workflow}

The extension integrates with TYPO3's centralized Crowdin translation system:

**Configuration** (`crowdin.yml`):

```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](https://crowdin.com/project/typo3-cms)
1.  TYPO3 translation coordinators review submissions
1.  Approved translations automatically sync to repository
1.  Changes included in next extension release

**Adding New Languages:**

To add a new language:

1.  Create language files following naming convention: `{lang}.locallang*.xlf`
1.  Copy structure from English source files
1.  Update `crowdin.yml` with new language patterns
1.  Submit to Crowdin for community translation

See [CONTRIBUTING.md](https://github.com/netresearch/t3x-nr-textdb/blob/main/CONTRIBUTING.md#how-to-contribute-translations) for detailed translation contribution guidelines.

## ViewHelpers {#dev-viewhelpers}

### TextDB ViewHelper {#textdb-viewhelper}

-   **class TextdbViewHelper**

    Main ViewHelper for displaying translations in Fluid templates.

    -   *Namespace:* `Netresearch\NrTextdb\ViewHelpers`
    -   *Extends:* `TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper`

    **Usage:**

    ```html
    {namespace textdb=Netresearch\NrTextdb\ViewHelpers}

    <textdb:textdb
        component="website"
        type="label"
        placeholder="welcome.message"
    />
    ```

    **Parameters:**

    -   **component**

        -   *Type:* string
        -   *Required:* true

        Component identifier for organizing translations (e.g., "website", "shop", "checkout").

    -   **type**

        -   *Type:* string
        -   *Required:* true
        -   *Default:* `P`

        Type identifier categorizing the translation (e.g., "label", "message", "error", "button").

    -   **placeholder**

        -   *Type:* string
        -   *Required:* true

        Unique translation key within the component and type context.

    -   **environment**

        -   *Type:* string
        -   *Required:* false
        -   *Default:* `default`

        Environment name for contextual translations.

    **Output:**

Returns the translated text for the current page language.

**Example:**

```html
<!-- 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 {#translate-viewhelper}

Alternative ViewHelper compatible with f:translate interface.

**Usage:**

```html
{namespace textdb=Netresearch\NrTextdb\ViewHelpers}

<textdb:translate key="LLL:EXT:my_ext:path.to.key" />
```

**Component Configuration:**

Set component in controller:

```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
1.  Create TextDB record automatically
1.  Use TextDB record on subsequent requests

## Services API {#dev-services}

### TranslationService {#translationservice}

-   **class TranslationService**

    Core service for translation management and retrieval.

    -   *Namespace:* `Netresearch\NrTextdb\Service`

    **Dependency Injection:**

    ```php
    use Netresearch\NrTextdb\Service\TranslationService;

    class MyClass
    {
        public function __construct(
            private readonly TranslationService $translationService
        ) {}
    }
    ```

    **Methods:**

    translate()

    ```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:**

    ```php
    $value = $this->translationService->translate(
        placeholder: 'welcome.message',
        typeName: 'label',
        componentName: 'website',
        environmentName: 'default',
    );
    ```

    createTranslation()

    ```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:**

    ```php
    $translation = $this->translationService->createTranslation(
        environment: $environment,
        component: $component,
        type: $type,
        placeholder: 'cart.add',
        sysLanguageUid: 0,
        value: 'Add to cart',
    );
    ```

    getAllLanguages()

    ```php
    public function getAllLanguages(): array
    ```

    Returns all configured site languages from the first available site.

    -   *returns:* Array of `SiteLanguage` objects

### ImportService {#importservice}

Service for importing XLIFF files.

**Injection:**

```php
use Netresearch\NrTextdb\Service\ImportService;

public function __construct(
    private readonly ImportService $importService
) {}
```

**Methods:**

#### importFile() {#importfile}

```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:**

```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() {#importentry}

```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.

## Repositories {#dev-repositories}

### TranslationRepository {#translationrepository}

Repository for translation records.

**Custom Methods:**

#### findAllByComponentTypePlaceholderValueAndLanguage() {#findallbycomponenttypeplaceholdervalueandlanguage}

```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() {#findbyenvironmentcomponenttypeplaceholderandlanguage}

```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() {#findbyenvironmentcomponenttypeandplaceholder}

```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:**

```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
    }
}
```

## Domain Models {#dev-domain-models}

### Translation Model {#translation-model}

Main translation entity.

**Properties:**

```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:**

```php
$translation = new Translation();
$translation->setPlaceholder('welcome.message');
$translation->setValue('Welcome!');
$translation->setComponent($component);
$translation->setType($type);

echo $translation->getValue(); // "Welcome!"
```

### Component Model {#component-model}

```php
class Component extends AbstractValueObject
{
    protected string $name = '';
}
```

### Type Model {#type-model}

```php
class Type extends AbstractValueObject
{
    protected string $name = '';
}
```

### Environment Model {#environment-model}

```php
class Environment extends AbstractValueObject
{
    protected string $name = '';
}
```

## Console Commands {#dev-commands}

### ImportCommand {#importcommand}

CLI command for importing translations.

**Location:** `Classes/Command/ImportCommand.php`

**Usage:**

```bash
vendor/bin/typo3 nr_textdb:import [extensionKey] [--override|-o]
```

**Configuration:**

```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:**

```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;
    }
}
```

## Events & Hooks {#dev-events}

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 {#dev-api-examples}

### Example 1: Programmatic Translation Management {#example-1-programmatic-translation-management}

```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 {#example-2-custom-export-functionality}

```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 {#example-3-frontend-integration}

```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;
    }
}
```

## Testing {#dev-testing}

### Unit Testing {#unit-testing}

Example unit test for Translation model:

```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 {#functional-testing}

Example functional test for repository:

```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);
    }
}
```

## Extension Points {#dev-extension-points}

### Extending the TranslationService {#extending-the-translationservice}

```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 {#custom-viewhelper}

```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',
        );
    }
}
```

## Best Practices {#dev-best-practices}

### Naming Conventions {#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 {#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 {#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 {#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 {#dev-troubleshooting}

### ViewHelper Not Working {#viewhelper-not-working}

```php
// Check namespace registration
{namespace textdb=Netresearch\NrTextdb\ViewHelpers}

// Verify storage PID configuration
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['nr_textdb']['textDbPid']
```

### Service Injection Failing {#service-injection-failing}

```yaml
# Ensure Services.yaml is configured
services:
    _defaults:
        autowire: true

    MyVendor\MyExt\MyClass:
        public: true  # If accessed via GeneralUtility::makeInstance
```

### Translation Not Found {#translation-not-found}

```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?
```

## Resources {#dev-resources}

-   **GitHub Repository:** [https://github.com/netresearch/t3x-nr-textdb](https://github.com/netresearch/t3x-nr-textdb)
-   **TYPO3 Extension Repository:** [https://extensions.typo3.org/extension/nr_textdb](https://extensions.typo3.org/extension/nr_textdb)
-   **TYPO3 Core API:** [https://docs.typo3.org/m/typo3/reference-coreapi/](https://docs.typo3.org/m/typo3/reference-coreapi/)
-   **Extbase Documentation:** [https://docs.typo3.org/m/typo3/book-extbasefluid/](https://docs.typo3.org/m/typo3/book-extbasefluid/)
-   **Fluid ViewHelper Reference:** [https://docs.typo3.org/other/typo3/view-helper-reference/](https://docs.typo3.org/other/typo3/view-helper-reference/)
