Localization in Extbase
An Extbase plugin that lists records will show different records in different languages — and sometimes a different number of them. This chapter explains what decides that, starting from the setting you are most likely to have inherited and working towards the cases you control in code.
On this page
Note
Whatever this chapter says about an object does not automatically hold for the objects it relates to. Relations follow different rules almost everywhere, so each section states separately what happens to them.
How the site configuration decides the language of Extbase records
Extbase does not decide which language your records are fetched in. It reads the language aspect from the Context API and follows it. In the frontend that aspect is built from the site configuration.
The setting that decides it is fallbackType, which each language of a site carries:
fallback |
A visitor requesting Polish sees |
|---|---|
free | Only records actually stored as Polish records. No translation handling at all. |
fallback | Polish conferences, plus the English originals of those that have no Polish translation. |
strict | Only conferences that exist in Polish: translations of an English original, and conferences created directly in Polish without one. Untranslated ones disappear from the list. |
If a site language sets no fallback, it is strict.
Records marked as all languages (
sys_) are returned
in every language, whichever setting is in use.
Language overlay types behind fallbackType
Internally each of those settings becomes an overlay type on the language
aspect. The names appear in query settings, in
\TYPO3\ and in most discussions of the
topic, so it is worth knowing which is which:
fallback |
Overlay type |
|---|---|
free |
Language |
fallback |
Language |
strict |
Language |
A fourth overlay type,
Language, exists in the class
but no site configuration produces it. It appears only where an aspect is
constructed in PHP, as described in
Deciding the language per query.
See also
Overlay types — what each of the four does, independently of Extbase.
Changed in version 14.3
Extbase previously ignored fallback when fetching records and always
behaved like fallback. It now follows the site configuration, so a
strict site probably returns fewer records than before:
find on an untranslated record returns
null
rather
than the default language record, and untranslated related records are
dropped from relations. See Important: #88886 Extbase persistence
respects the language overlay type.
To keep a single query behaving as before, set an aspect with
OVERLAYS_ on it as shown in Deciding the language per
query.
The site configuration governs the records a repository fetches directly. It does not fully govern their relations.
On a free site, a query performs no translation handling — but the relations
of the records it returns still do. A conference fetched without translation
handling can still come back with its categories translated. This is
deliberate: relations are usually stored against the default language record,
so Extbase would otherwise return nothing for them.
The practical consequence is that free mode is not "translation handling
switched off" for your whole object graph, only for its roots.
Setting the language for a single Extbase query
When you maintain the extension, a single query can depart from the site configuration. Every query carries query settings, and the language aspect is one of them.
Setting a language aspect on Extbase query settings
Put a
Language on the query
settings. This is the supported way to fetch records in a language, or with a
translation behavior, that differs from the one the site asked for.
Give the repository one method that accepts a finished aspect, rather than one method per way of choosing a language:
<?php
namespace MyVendor\MyExtension\Domain\Repository;
use MyVendor\MyExtension\Domain\Model\Conference;
use TYPO3\CMS\Core\Context\LanguageAspect;
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
use TYPO3\CMS\Extbase\Persistence\Repository;
class ConferenceRepository extends Repository
{
/**
* Takes a ready-made language aspect, so the caller decides the language.
* Usable from the frontend, a backend module and the command line alike.
*
* @return QueryResultInterface<Conference>
*/
public function findAllForLanguageAspect(
LanguageAspect $languageAspect,
): QueryResultInterface {
$query = $this->createQuery();
$query->getQuerySettings()->setLanguageAspect($languageAspect);
return $query->execute();
}
}
The repository then makes no assumption about where the language came from, and the same method serves a frontend plugin, a backend module and a command alike. Choosing the language is a decision for the caller, and there are two ways to make it.
Deriving the language aspect from a site language
If your site configuration declares the language configuration you want, create the language aspect from it. If there is none, you can create the aspect manually.
<?php
namespace MyVendor\MyExtension\Controller;
use MyVendor\MyExtension\Domain\Repository\ConferenceRepository;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Context\LanguageAspectFactory;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
class ConferenceController extends ActionController
{
public function __construct(
protected readonly ConferenceRepository $conferenceRepository,
) {}
/**
* Lists conferences in a chosen language with that language's own
* translation behavior.
*/
public function listInLanguageAction(int $languageId): ResponseInterface
{
$site = $this->request->getAttribute('site');
$languageAspect = LanguageAspectFactory::createFromSiteLanguage(
$site->getLanguageById($languageId),
);
$this->view->assign(
'conferences',
$this->conferenceRepository->findAllForLanguageAspect($languageAspect),
);
return $this->htmlResponse();
}
}
Language is the same call the
frontend uses internally. It reads the language UID, the fallback and
the fallback chain from that one site language, so the three values stay
consistent with each other.
Building a custom language aspect in PHP
Constructing the aspect by hand, as the action translated does,
is the right move only when you deliberately want behavior outside what
a site language can express.
Language is the
clearest case: it returns translations that have a default language original,
and leaves out records that exist only in the requested language without
a valid default language parent.
<?php
namespace MyVendor\MyExtension\Controller;
use MyVendor\MyExtension\Domain\Repository\ConferenceRepository;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Context\LanguageAspect;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
class ConferenceController extends ActionController
{
public function __construct(
protected readonly ConferenceRepository $conferenceRepository,
) {}
/**
* Lists only genuine translations: conferences that have a default
* language original, leaving out those that exist without a default
* language parent. No site configuration produces OVERLAYS_ON, so the
* aspect is built here rather than derived from a site language.
*/
public function translatedOnlyAction(int $languageId): ResponseInterface
{
$languageAspect = new LanguageAspect(
$languageId, // language id
$languageId, // content id
LanguageAspect::OVERLAYS_ON,
[],
);
$this->view->assign(
'conferences',
$this->conferenceRepository->findAllForLanguageAspect($languageAspect),
);
return $this->htmlResponse();
}
}
The constructor takes four parameters, each documented as a property of the aspect in the Context API reference: the requested language, the language records are fetched in, the overlay type and the fallback chain. The first two differ only when a page requested in one language should show the content of another.
Records marked as all languages are returned whichever overlay type you set, so this is not a way to exclude them.
Warning
Constructing a
Language manually
with languageUid, overlay type and fallback chain in a combination not
declared by any site configuration can lead to unexpected results in the
delivered record set. Make sure to verify especially the fallback chain in
such a case, as it relies on languageUids that can change in the site
configuration without the code ever learning about it.
Both routes are used elsewhere in this manual: from outside the frontend, where no site implies the language, and when reading records that belong to another site.
uid and _localizedUid of localized Extbase objects
Once translation handling is involved, the uid of a domain object is no
longer simply the uid of the row it came from. Extbase keeps both, in
two properties every domain object has:
uid- The identifier of the default language record. This is the one to use when building links or storing a reference, because it stays the same in every language.
_localizedUid - The identifier of the record the values actually came from — the translation, where one was used.
For a conference stored as
uid: in the default language and
uid: in its Polish translation, a query in Polish returns:
| Translation handling | Default language record | Translated record |
|---|---|---|
On (strict, fallback) | uid: 2, _localized | uid: 2, _localized |
Off (free) | uid: 2, _localized | uid: 11, _localized |
The difference matters when a record is passed back into a query or a link: on
a free site the object carries the translated record's own identifier, while
everywhere else it carries the default language one.
A third property,
_language, holds the language the record belongs
to. It is the property to set when writing a record in a specific
language.
Hint
If your project uses
typo3/cms-workspaces
there is yet another
additional property,
_versioned. Refer to
Versioning in workspaces for details on
workspace overlays.
Fetching Extbase records in all languages
set removes the language restriction from the
query. Records of every language are returned side by side, so a conference
existing in three languages is returned three times.
This is what you want for a listing that deliberately spans languages, such as a backend overview of all translations of a record. In a frontend list it looks like duplicates.
An aspect set on a query applies to the records that query returns. Relations are fetched by separate queries, which Extbase configures itself, and it overrules parts of what you set:
setandRespect Storage Page (false) setare always applied to relation queries, whatever the parent query said.Respect Sys Language (false) OVERLAYS_is replaced byOFF OVERLAYS_, so relations are translation-handled even when the parent query is not.MIXED - The language of the parent record is passed down, so relations are fetched in the language of the record holding them rather than the language of the request. Records marked as all languages are exempt.
The overlay type and fallback chain you set are otherwise carried over. In practice this means you can influence relations, but you cannot switch translation handling off for them.
Localization beyond reading in the frontend
The rules above describe a plugin reading records in a rendered frontend request. Three situations depart from that, each on its own page:
- Reading localized records across sites
- Reading records that belong to another site, where language IDs and fallback settings may no longer be the same as for the site you are currently handling.
- Writing records from the frontend
- Creating records from frontend forms, where Extbase decides the language itself and cannot produce translations.
- Localization outside the frontend
- Backend modules, command line commands and middlewares, where the site and language the frontend would have supplied are missing.
Translating labels in Extbase extensions
This chapter covers how Extbase handles records across languages. Translating the labels of an extension — button captions, flash messages, validation errors — is a separate topic and handled on dedicated pages:
See also
- Translating labels in Extbase — in controllers and other PHP code
- LocalizationUtility API reference
— all parameters of
translate() - Translating labels in Fluid —
the
<f:ViewHelpertranslate>