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.

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:

fallbackType 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 fallbackType, it is strict.

Records marked as all languages ( sys_language_uid = -1 ) 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\CMS\Core\Context\LanguageAspect and in most discussions of the topic, so it is worth knowing which is which:

fallbackType Overlay type
free LanguageAspect::OVERLAYS_OFF
fallback LanguageAspect::OVERLAYS_MIXED
strict LanguageAspect::OVERLAYS_ON_WITH_FLOATING

A fourth overlay type, LanguageAspect::OVERLAYS_ON , 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.

Changed in version 14.3

Extbase previously ignored fallbackType when fetching records and always behaved like fallback. It now follows the site configuration, so a strict site probably returns fewer records than before: findByUid() 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_MIXED on it as shown in Deciding the language per query.

What this means for relations

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

EXT:my_extension/Classes/Domain/Repository/ConferenceRepository.php
<?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();
  }
}
Copied!

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.

EXT:my_extension/Classes/Controller/ConferenceController.php
<?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();
  }
}
Copied!

LanguageAspectFactory::createFromSiteLanguage() is the same call the frontend uses internally. It reads the language UID, the fallbackType 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 translatedOnlyAction does, is the right move only when you deliberately want behavior outside what a site language can express. LanguageAspect::OVERLAYS_ON 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.

EXT:my_extension/Classes/Controller/ConferenceController.php
<?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();
  }
}
Copied!

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.

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:2 in the default language and uid:11 in its Polish translation, a query in Polish returns:

Translation handling Default language record Translated record
On (strict, fallback) uid: 2, _localizedUid: 2 uid: 2, _localizedUid: 11
Off (free) uid: 2, _localizedUid: 2 uid: 11, _localizedUid: 11

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, _languageUid , holds the language the record belongs to. It is the property to set when writing a record in a specific language.

Fetching Extbase records in all languages 

setRespectSysLanguage(false) 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.

What this means for relations

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:

  • setRespectStoragePage(false) and setRespectSysLanguage(false) are always applied to relation queries, whatever the parent query said.
  • OVERLAYS_OFF is replaced by OVERLAYS_MIXED , so relations are translation-handled even when the parent query is not.
  • 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: