Feature: #93334 - Translation Domain Mapping 

See forge#93334

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, like "backend" for "EXT:backend".

This synax is designed to improve readability, remove a clear reference to the used file extension and to add convenience for new developers and integrators: The previous locallang.xlf convention as well as the name has been removed in favor of a more generic "messages" resource name, which is common for localization systems or Symfony-based applications. This is also where the term "translation domain" stems from.

Example:

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

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

Translation Domain Format 

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

As mentioned, the resource part will leave out previous historical namings, especially locallang.xlf and the locallang_ prefix.

The actual identifier within the resource is separated by a colon.

Format 

Example usage of "package.resource:identifier"
$languageService->sL('backend.toolbar:save');
// Resolves to: EXT:backend/Resources/Private/Language/locallang_toolbar.xlf and
// returns the translated "save" identifier.
Copied!

Domain Resolution 

Deterministic File-Based 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
  2. A domain name is generated from each file name
  3. The domain-to-file mapping is cached in cache.l10n
  4. Subsequent requests use the cached mapping

This ensures domain names always correspond to existing files and prevents 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 

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 

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

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

    • locallang.xlf.messages
    • locallang_toolbar.xlf.toolbar
    • locallang_sudo_mode.xlf.sudo_mode
  3. Subdirectories use dot notation:

    • Form/locallang_tabs.xlf.form.tabs
  4. Site Set labels receive the .sets prefix:

    • Configuration/Sets/Felogin/labels.xlf.sets.felogin
  5. Case conversion:

    • UpperCamelCase → snake_case (SudoModesudo_mode)
    • snake_case → preserved (sudo_modesudo_mode)
  6. Locale prefixes are irrelevant for the resource identifier resolving. These prefixes will be properly evaluated internally for later locale-based translations:

    • (de.locallang.xlfmessages)
    • (de-AT.tabs.xlftabs)

Examples:

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
Copied!

Usage 

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

$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');
Copied!

Domain-based references are shorter and expose less implementation detail compared to full file paths.

CLI Command 

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

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

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

Output:

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

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

PSR-14 Event 

The event BeforeLabelResourceResolvedEvent is dispatched after domain generation, allowing customization of domain names.

Event: \TYPO3\CMS\Core\Localization\Event\BeforeLabelResourceResolvedEvent

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 

Event listener implementation:

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';
    }
}
Copied!

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 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>
Copied!

and utilized via the module definition:

EXT:my_extension/Configuration/Backend/Modules.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',
            ],
        ],
    ],
];
Copied!

Now, labels can use more speaking identifiers:

EXT:my_extension/Resources/Private/Language/Module/mymodule.xlf
<?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>
Copied!
EXT:my_extension/Configuration/Backend/Modules.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',
            ],
        ],
    ],
];
Copied!

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)
  2. Use understandable XLIFF identifiers: - "title" instead of "mlang_tabs_tab" - "short_description" instead of "mlang_labels_tablabel" - "description" instead of "mlang_labels_tabdescr"
  3. 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. TYPO3 Core also uses singular module language containers like workspaces.module instead of workspaces.modules.workspaces.

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 file system operations.

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

TYPO3 Core will slowly migrate internal references to use translation domains over time, as this increases readability, especially in Fluid templates, or TCA references.

Technical components:

  • TranslationDomainMapper - Maps domains to file paths, manages cache
  • LabelFileResolver - Discovers label files and handles locale resolution
  • LocalizationFactory - Integrates domain resolution transparently

The TranslationDomainMapper automatically detects file references (EXT: prefix) and passes them through unchanged.