Localization in PHP

Sometimes you have to localize a string in PHP code, for example inside of a controller or a user function.

Which method of localization to use depends on the current context:

Localization in plain PHP

Localization in frontend context

In plain PHP use the class LanguageServiceFactory to create a LanguageService from the current site language:

<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend;

use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;

/**
 * File EXT:my_extension/Classes/UserFunction/MyUserFunction.php
 */
final class MyUserFunction
{
    private LanguageService $languageService;

    public function __construct(
        private readonly LanguageServiceFactory $languageServiceFactory,
    ) {
    }

    private function getLanguageService(
        ServerRequestInterface $request
    ): LanguageService {
        return $this->languageServiceFactory->createFromSiteLanguage(
            $request->getAttribute('language')
            ?? $request->getAttribute('site')->getDefaultLanguage()
        );
    }

    public function main(
        string $content,
        array $conf,
        ServerRequestInterface $request
    ): string {
        $this->languageService = $this->getLanguageService($request);
        return $this->languageService->getLL(
            'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:something.'
        );
    }
}

Dependency injection should be available in most contexts where you need translations. Also the current request is available in entry point such as custom non-Extbase controllers, user functions, data processors etc.

Localization in backend context

In the backend context you can use the global variable $GLOBALS['LANG'] which contains the LanguageService.

<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Backend;

/**
 * File EXT:my_extension/Classes/Backend/MyBackendClass.php
 */
final class MyBackendClass
{
    private function translateSomething(string $lll): string
    {
        return $GLOBALS['LANG']->sL($lll);
    }

    // ...
}

Attention

During development you are usually logged into the backend. So the global variable $GLOBALS['LANG'] might be available in the frontend. Once logged out it is usually not available. Never depend on $GLOBALS['LANG'] in the frontend unless you know what you are doing.

Localization without context

If you should happen to be in a context where none of these are available, for example a static function, you can still do translations:

<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Utility;

use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;

/**
 * File EXT:my_extension/Classes/Utility/MyUtility.php
 */
final class MyUtility
{
    private static function translateSomething(string $lll): string
    {
        $languageServiceFactory = GeneralUtility::makeInstance(
            LanguageServiceFactory::class
        );
        // As we are in a static context we cannot get the current request in
        // another way this usually points to general flaws in your software-design
        $request = $GLOBALS['TYPO3_REQUEST'];
        $languageService = $languageServiceFactory->createFromSiteLanguage(
            $request->getAttribute('language')
            ?? $request->getAttribute('site')->getDefaultLanguage()
        );
        return $languageService->sL($lll);
    }
}

Localization in Extbase

In Extbase context you can use the method TYPO3CMSExtbaseUtilityLocalizationUtility::translate($key, $extensionName).

This method requires the localization key as the first and the extension's name as optional second parameter. For all available parameters see below. Then the corresponding text in the current language will be loaded from this extension's locallang.xlf file.

The method translate() takes translation overrides from TypoScript into account. See Changing localized terms using TypoScript.

Example

In this example the content of the flash message to be displayed in the backend gets translated:

Class T3docs\Examples\Controller\ModuleController
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;

class ModuleController extends ActionController implements LoggerAwareInterface
{
    /**
     * Adds a count of entries to the flash message
     */
    public function countAction(string $tablename = 'pages'): ResponseInterface
    {
        $count = $this->tableInformationService->countRecords($tablename);

        $message = LocalizationUtility::translate(
            key: 'record_count_message',
            extensionName: 'examples',
            arguments: [$count, $tablename]
        );

        $this->addFlashMessage(
            messageBody: $message,
            messageTitle: 'Information',
            severity: ContextualFeedbackSeverity::INFO
        );
        return $this->redirect('flash');
    }
}

The string in the translation file is defined like this:

<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<!-- EXT:examples/Resources/Private/Language/locallang.xlf -->
<xliff version="1.0">
    <file source-language="en" datatype="plaintext" original="messages" date="2013-03-09T18:44:59Z" product-name="examples">
        <header />
        <body>
            <trans-unit id="new_relation" xml:space="preserve">
                <source>Content element "%1$s" (uid: %2$d) has the following relations:</source>
            </trans-unit>
        </body>
    </file>
</xliff>

The arguments will be replaced in the localized strings by the PHP function sprintf.

This behaviour is the same like in a Fluid translate ViewHelper with arguments.

Examples

Provide localized strings via JSON by a middleware

In the following example we use the language service API to provide a list of localized season names. This list could then be loaded in the frontend via Ajax.

You can finde the complete example on GitHub, EXT:examples, HaikuSeasonList.

As we do not need a full frontend context with TypoScript the JSON is returned by a PSR-15 middleware.

Beside other factories needed by our response, we inject the LanguageServiceFactory with constructor dependency injection.

Class T3docs\Examples\Middleware\HaikuSeasonList
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;

/**
 * This middleware can be used to retrieve a list of seasons with their according translation.
 * To get the correct translation the URL must be within a base path defined in site
 * handling. Some examples:
 * "/en/haiku-season-list.json" for English translation (if /en is the configured base path)
 * "/de/haiku-season-list.json" for German translation (if /de is the configured base path)
 * If the base path is not available in the according site the default language will be used.
 */
final class HaikuSeasonList implements MiddlewareInterface
{
    public function __construct(
        private readonly LanguageServiceFactory $languageServiceFactory,
        private readonly ResponseFactoryInterface $responseFactory,
        private readonly StreamFactoryInterface $streamFactory
    ) {
    }
}

The main method process() is called with a Psr\Http\Message\ServerRequestInterface as argument that can be used to detect the current language and is therefore passed on to the private method getSeasons() doing the actual translation:

Class T3docs\Examples\Middleware\HaikuSeasonList
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

final class HaikuSeasonList implements MiddlewareInterface
{
    private const URL_SEGMENT = '/haiku-season-list.json';

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        if (!str_contains($request->getUri()->getPath(), self::URL_SEGMENT)) {
            return $handler->handle($request);
        }

        $seasons = json_encode($this->getSeasons($request), JSON_THROW_ON_ERROR);

        return $this->responseFactory->createResponse()
            ->withHeader('Content-Type', 'application/json')
            ->withBody($this->streamFactory->createStream($seasons));
    }
}

Now we can let the \TYPO3\CMS\Core\Localization\LanguageServiceFactory to create a \TYPO3\CMS\Core\Localization\LanguageService from the request's language, falling back to the default language of the site.

The LanguageService can then be queried for the localized strings:

Class T3docs\Examples\Middleware\HaikuSeasonList
use Psr\Http\Message\ServerRequestInterface;

final class HaikuSeasonList implements MiddlewareInterface
{
    private const SEASONS = ['spring', 'summer', 'autumn', 'winter', 'theFifthSeason'];
    private const TRANSLATION_PATH = 'LLL:EXT:examples/Resources/Private/Language/PluginHaiku/locallang.xlf:season.';

    private function getSeasons(ServerRequestInterface $request): array
    {
        $languageService = $this->languageServiceFactory->createFromSiteLanguage(
            $request->getAttribute('language') ?? $request->getAttribute('site')->getDefaultLanguage()
        );

        $translatedSeasons = [];
        foreach (self::SEASONS as $season) {
            $translatedSeasons[$season] = $languageService->sL(self::TRANSLATION_PATH . $season);
        }

        return $translatedSeasons;
    }
}