---
title: "Feature: #93334 - Translation Domain Mapping"
manual: "TYPO3 Core Changelog"
version: "main"
permalink: "https://docs.typo3.org/permalink/changelog:feature-93334-1729000000"
source: "Changelog/14.0/Feature-93334-TranslationDomainMapping.rst"
typo3-version: "14.0"
typo3-major: 14
type: "feature"
issue: 93334
forge: "https://forge.typo3.org/issues/93334"
tags: ["PHP-API", "Localization", "ext:core"]
rendered: "2026-09-20T18:31:11+00:00"
---

# Feature: #93334 - Translation Domain Mapping {#feature-93334-1729000000}

See [forge#93334](https://forge.typo3.org/issues/93334)

## Description {#description}

Translation domains have been introduced as a shorter alternative to
file-based references for label resources (`.xlf` XLIFF files). The
syntax uses the format `package[.subdomain...].resource` and is fully backward
compatible with existing `LLL:EXT:` references. *Package* refers to the
extension key, such as "*backend*" for "EXT:backend".

This syntax is designed to improve readability, remove explicit references to
file extensions, and provide convenience for new developers and integrators.
The previous `locallang.xlf` convention has been replaced with a more
generic "*messages*" resource name, following common conventions in other
localization systems (for example Symfony). This is also where the term
*translation domain* originates.

Example:

```php
// Domain-based reference
$languageService->sL('backend.toolbar:save');

// Equivalent file-based reference (still supported)
$languageService->sL(
    'LLL:EXT:backend/Resources/Private/Language/locallang_toolbar.xlf:save'
);
```

> [!NOTE]
> The existing syntax and naming
> (`LLL:EXT:extension/Resources/Private/Language/locallang.xlf:label`)
> will remain available without deprecation for a long time.

## Translation Domain Format {#feature-93334-translation-domain-format}

The format defines two parts: the *package part* (extension key) and the
*resource part*, separated by a dot.

The resource part omits historical namings such as `locallang.xlf` and the
`locallang_` prefix. The actual label identifier is separated by a colon.

### Format {#format}

**Example usage of "package.resource:identifier"**

```php
$languageService->sL('backend.toolbar:save');
// Resolves to: EXT:backend/Resources/Private/Language/locallang_toolbar.xlf
// and returns the translated "save" identifier.
```

## Domain Resolution {#feature-93334-domain-resolution}

### Deterministic File-Based Mapping {#feature-93334-deterministic-mapping}

Translation domains are resolved deterministically by scanning the file
system. When a domain is first requested for a package:

1.  All label files in `Resources/Private/Language/` are discovered.
1.  A domain name is generated from each file name.
1.  The domain-to-file mapping is cached in `cache.l10n`.
1.  Subsequent requests use the cached mapping.

This ensures that domain names always correspond to existing files and avoids
speculative file system lookups.

When there are filename conflicts such as `locallang_db.xlf` and
`db.xlf`, then `locallang_db.xlf` will be ignored.

### Performance Characteristics {#feature-93334-performance}

The implementation reduces file system operations compared to traditional
file-based lookups, as all label files within an extension are discovered once.

### Domain Generation Rules {#feature-93334-domain-rules}

Domain names are generated from file paths using these transformation rules:

1.  The base path `Resources/Private/Language/` is omitted.
1.  Standard filename patterns:

    -   `locallang.xlf` → `.messages`
    -   `locallang_toolbar.xlf` → `.toolbar`
    -   `locallang_sudo_mode.xlf` → `.sudo_mode`
1.  Subdirectories use dot notation:

    -   `Form/locallang_tabs.xlf` → `.form.tabs`
1.  Site Set labels receive the `.sets` prefix:

    -   `Configuration/Sets/Felogin/labels.xlf` → `.sets.felogin`
1.  Case conversion:

    -   UpperCamelCase → snake_case (`SudoMode` → `sudo_mode`)
    -   snake_case → preserved (`sudo_mode` → `sudo_mode`)
1.  Locale prefixes are ignored for domain name generation but properly
    evaluated for locale-specific translations:

    -   (`de.locallang.xlf` → `messages`)
    -   (`de-AT.tabs.xlf` → `tabs`)

Examples:

```text
File Path                                     → Domain
────────────────────────────────────────────────────────────
EXT:backend/.../locallang.xlf                 → backend.messages
EXT:backend/.../locallang_toolbar.xlf         → backend.toolbar
EXT:core/.../Form/locallang_tabs.xlf          → core.form.tabs
EXT:felogin/Configuration/Sets/.../labels.xlf → felogin.sets.felogin
```

> [!IMPORTANT]
> The domain name `messages` is currently evaluated for both the
> legacy file name `locallang.xlf` but also for new files `messages.xlf`.
> If a file `messages.xlf` is present, this means the `locallang.xlf`
> will never be automatically evaluated for the resulting `messages` domain.
>
> It is recommended to avoid having both files in the same directory, unless
> both contain the same label contents, as no merging of these two
> files is performed.

## Usage {#feature-93334-usage}

The translation domain system integrates with the existing
`\TYPO3\CMS\Core\Localization\LanguageService` API. Both domain-based and
file-based references are supported:

```php
use TYPO3\CMS\Core\Localization\LanguageService;

$languageService = $this->languageServiceFactory->createFromSiteLanguage(
    $request->getAttribute('language')
);

// Domain-based reference
$label = $languageService->sL('backend.toolbar:menu.item');

// Another domain-based reference
$label = $languageService->sL('backend.messages:button.save');

// Traditional file reference (still supported)
$label = $languageService->sL(
    'LLL:EXT:backend/Resources/Private/Language/locallang.xlf:button.save'
);
```

Domain-based references are shorter and reveal less implementation detail than
full file paths.

## CLI Command {#feature-93334-cli}

The development command `bin/typo3 language:domain:list` lists all available
translation domains along with their available translations and label counts:

```bash
# List domains in active extensions
php bin/typo3 language:domain:list

# Filter by extension
php bin/typo3 language:domain:list --extension=backend
```

Output:

```text
+--------------------+---------------------------------------+----------+
| Translation Domain | Label Resource                        | # Labels |
+--------------------+---------------------------------------+----------+
| backend.messages   | EXT:backend/.../locallang.xlf         | 84       |
| backend.toolbar    | EXT:backend/.../locallang_toolbar.xlf | 42       |
+--------------------+---------------------------------------+----------+
```

The **Labels** column displays the number of translatable labels within the
English source file.

On top of this, the development command `bin/typo3 language:domain:search`
can be used to search for specific label contents. Both commands are provided
in the `EXT:lowlevel` extension.

## PSR-14 Event {#feature-93334-psr14}

The event `\TYPO3\CMS\Core\Localization\Event\BeforeLabelResourceResolvedEvent`
is dispatched after domain generation, allowing customization of domain names.

The event provides these public properties:

-   `$packageKey` — The extension key (read-only).
-   `$domains` — An associative array mapping domain names to label files
    (modifiable): `array<string, string>`.

### Example {#example}

Event listener implementation:

```php
namespace MyVendor\MyExtension\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Localization\Event\BeforeLabelResourceResolvedEvent;

final readonly class CustomTranslationDomainResolver
{
    #[AsEventListener(identifier: 'my-extension/custom-domain-names')]
    public function __invoke(BeforeLabelResourceResolvedEvent $event): void
    {
        if ($event->packageKey !== 'my_extension') {
            return;
        }

        // Use file my_messages.xlf even if locallang.xlf is found
        $event->domains['my_extension.messages'] =
            'EXT:my_extension/Resources/Private/Language/my_messages.xlf';
    }
}
```

## Backend modules {#backend-modules}

Previously, backend module labels (including their title and description) were
defined in a file like this:

**EXT:my_extension/Resources/Private/Language/locallang_mod.xlf**

```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:my_extension/Resources/Private/Language/locallang_mod.xlf" date="2038-10-28T13:37:37Z" product-name="mymodule">
        <header/>
        <body>
            <trans-unit id="mlang_labels_tablabel">
                <source>My module</source>
            </trans-unit>
            <trans-unit id="mlang_labels_tabdescr">
                <source>Shows my module.</source>
            </trans-unit>
            <trans-unit id="mlang_tabs_tab">
                <source>My label</source>
            </trans-unit>
        </body>
    </file>
</xliff>
```

and utilized via the module definition:

**EXT:my_extension/Configuration/Backend/Modules.php**

```php
<?php
return [
    'my_module' => [
        'parent' => 'web',
        'position' => ['after' => 'web_list'],
        'access' => 'user',
        'path' => '/module/my-module',
        'iconIdentifier' => 'my-module-icon',
        'labels' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang_mod.xlf',
        'aliases' => ['web_MyModule'],
        'routes' => [
            '_default' => [
                'target' => MyController::class . '::handleRequest',
            ],
        ],
    ],
];
```

Now, labels can use more speaking identifiers:

**EXT:my_extension/Resources/Private/Language/Module/mymodule.xlf**

```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:my_extension/Resources/Private/Language/Modules/mymodule.xlf" date="2026-11-05T16:22:37Z" product-name="mymodule">
        <header/>
        <body>
            <trans-unit id="short_description">
                <source>My module</source>
            </trans-unit>
            <trans-unit id="description">
                <source>Shows my module.</source>
            </trans-unit>
            <trans-unit id="title">
                <source>My label</source>
            </trans-unit>
        </body>
    </file>
</xliff>
```

**EXT:my_extension/Configuration/Backend/Modules.php**

```php
<?php
return [
    'my_module' => [
        'parent' => 'web',
        'position' => ['after' => 'web_list'],
        'access' => 'user',
        'path' => '/module/my-module',
        'iconIdentifier' => 'my-module-icon',
        'labels' => 'my_extension.modules.my_module',
        'aliases' => ['web_MyModule'],
        'routes' => [
            '_default' => [
                'target' => MyController::class . '::handleRequest',
            ],
        ],
    ],
];
```

The naming for the short-hand translation domain for modules should follow
the following pattern as best practice:

-   `<extensionkey>.modules.<modulename>` \- **when multiple modules exist for an extension**.
    Both `extensionKey` and `modulename` should use lower snake case ("some_long_module_name"),
    ideally without underscores (`qrcode.modules.generator` is more readable than
    `qrcode.modules.backend_image_generator` for example). Files are put into
    `EXT:extensionkey/Resources/Private/Languages/Modules/modulename.xlf`.
-   `<extensionkey>.module` \- **single backend module only**
    The file is saved as `EXT:extensionkey/Resources/Private/Languages/module.xlf`.

To summarize, the key changes are:

1.  Use a speaking XLIFF file inside `/Resources/Private/Languages/Modules` (best practice, could be any sub-directory)
1.  Use understandable XLIFF identifiers:
    \- "title" instead of "mlang_tabs_tab"
    \- "short_description" instead of "mlang_labels_tablabel"
    \- "description" instead of "mlang_labels_tabdescr"
1.  Use short-form identifiers ("my_extension.modules.my_module" instead of "LLL:EXT:my_extension/Resources/Private/Language/locallang_mod.xlf")
    inside the `Backend/Modules.php` registration.

All TYPO3 Core backend modules that used the old label identifiers have been migrated to the new syntax, the utilized
files are now deprecated, see [deprecation](https://docs.typo3.org/permalink/changelog:deprecation-107938-1762181263). TYPO3 Core also uses
singular module language containers like `workspaces.module` instead of `workspaces.modules.workspaces`.

## Impact {#impact}

Translation domains provide a shorter, more readable alternative to file-based
label references. The implementation uses deterministic file-system scanning
with per-package caching to reduce lookups.

All existing `LLL:EXT:` file references continue to work. Translation domains
are optional and can be adopted incrementally. Both syntaxes can coexist in
the same codebase. This affects TypoScript, Fluid
`<f:translate>` usages, TCA configuration, and PHP code using the
`LanguageService` API.

TYPO3 Core will gradually migrate internal references to translation domains
over time, increasing readability—especially in Fluid templates or TCA
definitions.

Technical components:

-   **`\TYPO3\CMS\Core\Localization\TranslationDomainMapper`**

    Maps domains to file paths and manages the cache.

-   **`\TYPO3\CMS\Core\Localization\LabelFileResolver`**

    Discovers label files and handles locale resolution.

-   **`\TYPO3\CMS\Core\Localization\LocalizationFactory`**

    Integrates domain resolution transparently.

The `TranslationDomainMapper`
automatically detects `EXT:` file references and passes them through unchanged.
