TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,411 @@
|
||||
.. include:: /Includes.rst.txt
|
||||
|
||||
.. _feature-93334-1729000000:
|
||||
|
||||
============================================
|
||||
Feature: #93334 - Translation Domain Mapping
|
||||
============================================
|
||||
|
||||
See :issue:`93334`
|
||||
|
||||
Description
|
||||
===========
|
||||
|
||||
Translation domains have been introduced as a shorter alternative to
|
||||
file-based references for label resources (:file:`.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 :file:`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:
|
||||
|
||||
.. code-block:: 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.
|
||||
|
||||
.. _feature-93334-translation-domain-format:
|
||||
|
||||
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
|
||||
------
|
||||
|
||||
.. code-block:: php
|
||||
:caption: 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.
|
||||
|
||||
.. _feature-93334-domain-resolution:
|
||||
|
||||
Domain Resolution
|
||||
=================
|
||||
|
||||
.. _feature-93334-deterministic-mapping:
|
||||
|
||||
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 :directory:`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 that domain names always correspond to existing files and avoids
|
||||
speculative file system lookups.
|
||||
|
||||
When there are filename conflicts such as :file:`locallang_db.xlf` and
|
||||
:file:`db.xlf`, then :file:`locallang_db.xlf` will be ignored.
|
||||
|
||||
.. _feature-93334-performance:
|
||||
|
||||
Performance Characteristics
|
||||
---------------------------
|
||||
|
||||
The implementation reduces file system operations compared to traditional
|
||||
file-based lookups, as all label files within an extension are discovered once.
|
||||
|
||||
.. _feature-93334-domain-rules:
|
||||
|
||||
Domain Generation Rules
|
||||
-----------------------
|
||||
|
||||
Domain names are generated from file paths using these transformation rules:
|
||||
|
||||
1. The base path :directory:`Resources/Private/Language/` is omitted.
|
||||
|
||||
2. Standard filename patterns:
|
||||
|
||||
* :file:`locallang.xlf` → `.messages`
|
||||
* :file:`locallang_toolbar.xlf` → `.toolbar`
|
||||
* :file:`locallang_sudo_mode.xlf` → `.sudo_mode`
|
||||
|
||||
3. Subdirectories use dot notation:
|
||||
|
||||
* :file:`Form/locallang_tabs.xlf` → `.form.tabs`
|
||||
|
||||
4. Site Set labels receive the `.sets` prefix:
|
||||
|
||||
* :file:`Configuration/Sets/Felogin/labels.xlf` → `.sets.felogin`
|
||||
|
||||
5. Case conversion:
|
||||
|
||||
* UpperCamelCase → snake_case (`SudoMode` → `sudo_mode`)
|
||||
* snake_case → preserved (`sudo_mode` → `sudo_mode`)
|
||||
|
||||
6. 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:
|
||||
|
||||
.. code-block:: 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 :file:`locallang.xlf` but also for new files :file:`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.
|
||||
|
||||
.. _feature-93334-usage:
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
The translation domain system integrates with the existing
|
||||
:php:`\TYPO3\CMS\Core\Localization\LanguageService` API. Both domain-based and
|
||||
file-based references are supported:
|
||||
|
||||
.. code-block:: 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.
|
||||
|
||||
.. _feature-93334-cli:
|
||||
|
||||
CLI Command
|
||||
===========
|
||||
|
||||
The development command :bash:`bin/typo3 language:domain:list` lists all available
|
||||
translation domains along with their available translations and label counts:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# List domains in active extensions
|
||||
php bin/typo3 language:domain:list
|
||||
|
||||
# Filter by extension
|
||||
php bin/typo3 language:domain:list --extension=backend
|
||||
|
||||
Output:
|
||||
|
||||
.. code-block:: 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 :bash:`bin/typo3 language:domain:search`
|
||||
can be used to search for specific label contents. Both commands are provided
|
||||
in the `EXT:lowlevel` extension.
|
||||
|
||||
.. _feature-93334-psr14:
|
||||
|
||||
PSR-14 Event
|
||||
============
|
||||
|
||||
The event :php:`\TYPO3\CMS\Core\Localization\Event\BeforeLabelResourceResolvedEvent`
|
||||
is dispatched after domain generation, allowing customization of domain names.
|
||||
|
||||
The event provides these public properties:
|
||||
|
||||
* :php:`$packageKey` — The extension key (read-only).
|
||||
* :php:`$domains` — An associative array mapping domain names to label files
|
||||
(modifiable): :php:`array<string, string>`.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
Event listener implementation:
|
||||
|
||||
.. code-block:: 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
|
||||
===============
|
||||
|
||||
Previously, backend module labels (including their title and description) were
|
||||
defined in a file like this:
|
||||
|
||||
.. code-block:: xml
|
||||
:caption: EXT:my_extension/Resources/Private/Language/locallang_mod.xlf
|
||||
:emphasize-lines: 6,9,12
|
||||
|
||||
<?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:
|
||||
|
||||
.. code-block:: php
|
||||
:caption: EXT:my_extension/Configuration/Backend/Modules.php
|
||||
:emphasize-lines: 9
|
||||
|
||||
<?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:
|
||||
|
||||
.. code-block:: xml
|
||||
:caption: 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>
|
||||
|
||||
.. code-block:: php
|
||||
:caption: EXT:my_extension/Configuration/Backend/Modules.php
|
||||
:emphasize-lines: 10
|
||||
|
||||
<?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
|
||||
:file:`EXT:extensionkey/Resources/Private/Languages/Modules/modulename.xlf`.
|
||||
* `<extensionkey>.module` - **single backend module only**
|
||||
The file is saved as :file:`EXT:extensionkey/Resources/Private/Languages/module.xlf`.
|
||||
|
||||
To summarize, the key changes are:
|
||||
|
||||
#. Use a speaking XLIFF file inside :directory:`/Resources/Private/Languages/Modules` (best practice, could be any sub-directory)
|
||||
#. Use understandable XLIFF identifiers:
|
||||
- "title" instead of "mlang_tabs_tab"
|
||||
- "short_description" instead of "mlang_labels_tablabel"
|
||||
- "description" instead of "mlang_labels_tabdescr"
|
||||
#. Use short-form identifiers ("my_extension.modules.my_module" instead of "LLL:EXT:my_extension/Resources/Private/Language/locallang_mod.xlf")
|
||||
inside the :file:`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 :ref:`deprecation <deprecation-107938-1762181263>`. 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 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
|
||||
:fluid:`<f:translate>` usages, TCA configuration, and PHP code using the
|
||||
:php-short:`\TYPO3\CMS\Core\Localization\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:
|
||||
|
||||
:php:`\TYPO3\CMS\Core\Localization\TranslationDomainMapper`
|
||||
Maps domains to file paths and manages the cache.
|
||||
:php:`\TYPO3\CMS\Core\Localization\LabelFileResolver`
|
||||
Discovers label files and handles locale resolution.
|
||||
:php:`\TYPO3\CMS\Core\Localization\LocalizationFactory`
|
||||
Integrates domain resolution transparently.
|
||||
|
||||
The :php-short:`\TYPO3\CMS\Core\Localization\TranslationDomainMapper`
|
||||
automatically detects `EXT:` file references and passes them through unchanged.
|
||||
|
||||
.. index:: PHP-API, Localization, ext:core
|
||||
Reference in New Issue
Block a user