---
title: "Developer Manual"
manual: "TextDB"
version: "4.1"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-textdb:developer@4.1"
source: "Developer/Index.rst"
rendered: "2026-10-31T14:42:22+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. When that
language has no usable entry, the ViewHelper falls back as described in
[Lookup Order and Language Fallback](https://docs.typo3.org/permalink/netresearch/nr-textdb:dev-viewhelpers-fallback@4.1).

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

### Lookup Order and Language Fallback {#dev-viewhelpers-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.
1.  `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.
1.  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.
1.  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.

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

    `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()

    ```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 [Lookup Order and Language Fallback](https://docs.typo3.org/permalink/netresearch/nr-textdb:dev-viewhelpers-fallback@4.1).

    -   *returns:* The value, or `null` if the current language has no usable entry

    translateFromFallbackLanguages()

    ```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
    [Lookup Order and Language Fallback](https://docs.typo3.org/permalink/netresearch/nr-textdb:dev-viewhelpers-fallback@4.1). 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:**

    ```php
    $value = $this->translationService->findTranslation('welcome.message', 'label', 'website', 'default')
        ?? $this->translationService->translateFromFallbackLanguages('welcome.message', 'label', 'website', 'default')
        ?? 'welcome.message';
    ```

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

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

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

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

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

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

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

## 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.
Hidden translations are returned too; callers that render a value check
`Translation::isHidden()` themselves.

#### 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\ImportResult;
use Netresearch\NrTextdb\Service\ImportService;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class CustomImportCommand extends Command
{
    public function __construct(
        private readonly ImportService $importService
    ) {
        parent::__construct();
    }

    protected function execute(
        InputInterface $input,
        OutputInterface $output
    ): int {
        $files = glob('/path/to/translations/*.xlf');

        foreach ($files as $file) {
            $result = new ImportResult();

            try {
                $this->importService->importFile($file, false, $result);
            } catch (\Throwable $exception) {
                $output->writeln('<error>' . $exception->getMessage() . '</error>');
            }

            $output->writeln(sprintf(
                'Imported: %d, Updated: %d from %s',
                $result->getImported(),
                $result->getUpdated(),
                $file,
            ));
        }

        return Command::SUCCESS;
    }
}
```

## Events & Hooks {#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/)
