Skip to content
Snippets Groups Projects
Commit 26bccb63 authored by Benni Mack's avatar Benni Mack Committed by Nikita Hovratov
Browse files

[TASK] Allow to override labels in LanguageService

The functionality to handle TypoScript
overrides for Labels is now moved
from Extbase LocalizationUtility
to LanguageService.

All tests now use proper XLF files
and also real TypoScript values,
in order to make the tests easier to
understand.

Instead of magic $LOCALLANG and $LOCALLANG_UNSET
properties, LocalizationUtility now keeps
the actual LanguageService objects in its storage.

The LanguageService->overrideLabels() method should be
used with care, as other instances of the object might
not have the overrides stored.

Resolves: #99559
Releases: main
Change-Id: I3e83695baebec545b5e6c5d8504df95950a543cc
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/77399


Tested-by: default avatarcore-ci <typo3@b13.com>
Tested-by: default avatarNikita Hovratov <nikita.h@live.de>
Reviewed-by: default avatarNikita Hovratov <nikita.h@live.de>
Reviewed-by: default avatarAnja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: default avatarAnja Leichsenring <aleichsenring@ab-softlab.de>
parent 03824a66
Branches
Tags
No related merge requests found
......@@ -67,6 +67,11 @@ class LanguageService
*/
protected array $labels = [];
/**
* @var string[][]
*/
protected array $overrideLabels = [];
protected Locales $locales;
protected LocalizationFactory $localizationFactory;
protected FrontendInterface $runtimeCache;
......@@ -137,8 +142,6 @@ class LanguageService
*/
protected function getLLL(string $index, array $localLanguage): string
{
// Get Local Language. Special handling for all extensions that
// read PHP LL files and pass arrays here directly.
if (isset($localLanguage[$this->lang][$index])) {
$value = is_string($localLanguage[$this->lang][$index])
? $localLanguage[$this->lang][$index]
......@@ -193,7 +196,11 @@ class LanguageService
}
$parts = explode(':', trim($restStr));
$parts[0] = $extensionPrefix . $parts[0];
$output = $this->getLLL($parts[1] ?? '', $this->readLLfile($parts[0]));
$labelsFromFile = $this->readLLfile($parts[0]);
if (is_array($this->overrideLabels[$parts[0]] ?? null)) {
$labelsFromFile = array_replace_recursive($labelsFromFile, $this->overrideLabels[$parts[0]]);
}
$output = $this->getLLL($parts[1] ?? '', $labelsFromFile);
$output .= $this->debugLL($input);
$this->runtimeCache->set($cacheIdentifier, $output);
return $output;
......@@ -223,7 +230,7 @@ class LanguageService
*/
protected function readLLfile(string $fileRef): array
{
$cacheIdentifier = 'labels_file_' . md5($fileRef . $this->lang);
$cacheIdentifier = 'labels_file_' . md5($fileRef . $this->lang . json_encode($this->languageDependencies));
$cacheEntry = $this->runtimeCache->get($cacheIdentifier);
if (is_array($cacheEntry)) {
return $cacheEntry;
......@@ -247,4 +254,37 @@ class LanguageService
$this->runtimeCache->set($cacheIdentifier, $localLanguage);
return $localLanguage;
}
/**
* Define custom labels which can be overridden for a given file. This is typically
* the case for TypoScript plugins.
*/
public function overrideLabels(string $fileRef, array $labels): void
{
$localLanguage = [
'default' => $labels['default'] ?? [],
];
if ($this->lang !== 'default') {
foreach ($this->languageDependencies as $language) {
// Populate the initial values with default, if no labels for the current language are given
if (!isset($localLanguage[$this->lang])) {
$localLanguage[$this->lang] = $localLanguage['default'];
}
if ($this->lang !== 'default' && isset($labels[$language])) {
$localLanguage[$this->lang] = array_replace_recursive($localLanguage[$this->lang], $labels[$language]);
}
}
}
$this->overrideLabels[$fileRef] = $localLanguage;
}
/**
* This is needed as Extbase LocalizationUtility allows to set custom dependencies.
* @internal This is not public API and might be removed at any time.
*/
public function setDependencies(array $dependencies): void
{
$this->languageDependencies = array_merge([$this->lang], $dependencies);
$this->languageDependencies = array_reverse($this->languageDependencies);
}
}
......@@ -18,10 +18,11 @@ declare(strict_types=1);
namespace TYPO3\CMS\Extbase\Utility;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Http\ApplicationType;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Localization\Locales;
use TYPO3\CMS\Core\Localization\LocalizationFactory;
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
......@@ -36,23 +37,6 @@ class LocalizationUtility
*/
protected static $locallangPath = 'Resources/Private/Language/';
/**
* Local Language content
*
* @var array
*/
protected static $LOCAL_LANG = [];
/**
* Contains those LL keys, which have been set to (empty) in TypoScript.
* This is necessary, as we cannot distinguish between a nonexisting
* translation and a label that has been cleared by TS.
* In both cases ['key'][0]['target'] is "".
*
* @var array
*/
protected static $LOCAL_LANG_UNSET = [];
/**
* @var \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface
*/
......@@ -75,7 +59,6 @@ class LocalizationUtility
// (from for example Fluid). Returning null allows null coalescing to a default value when that happens.
return null;
}
$value = null;
if (str_starts_with($key, 'LLL:')) {
$keyParts = explode(':', $key);
unset($keyParts[0]);
......@@ -90,39 +73,20 @@ class LocalizationUtility
}
$languageFilePath = static::getLanguageFilePath($extensionName);
}
$languageKeys = static::getLanguageKeys();
if ($languageKey === null) {
$languageKey = $languageKeys['languageKey'];
$languageKey = static::getLanguageKey();
}
if (empty($alternativeLanguageKeys)) {
$alternativeLanguageKeys = $languageKeys['alternativeLanguageKeys'];
}
static::initializeLocalization($languageFilePath, $languageKey, $alternativeLanguageKeys, $extensionName);
$languageService = static::initializeLocalization($languageFilePath, $languageKey, $alternativeLanguageKeys ?? [], $extensionName);
$resolvedLabel = $languageService->sL('LLL:' . $languageFilePath . ':' . $key);
$value = $resolvedLabel !== '' ? $resolvedLabel : null;
// The "from" charset of csConv() is only set for strings from TypoScript via _LOCAL_LANG
if (!empty(self::$LOCAL_LANG[$languageFilePath][$languageKey][$key][0]['target'])
|| isset(self::$LOCAL_LANG_UNSET[$languageFilePath][$languageKey][$key])
) {
// Local language translation for key exists
$value = self::$LOCAL_LANG[$languageFilePath][$languageKey][$key][0]['target'];
} elseif (!empty($alternativeLanguageKeys)) {
foreach ($alternativeLanguageKeys as $language) {
if (!empty(self::$LOCAL_LANG[$languageFilePath][$language][$key][0]['target'])
|| isset(self::$LOCAL_LANG_UNSET[$languageFilePath][$language][$key])
) {
// Alternative language translation for key exists
$value = self::$LOCAL_LANG[$languageFilePath][$language][$key][0]['target'];
break;
}
// Check if a value was explicitly set to "" via TypoScript, if so, we need to ensure that this is "" and not null
if ($extensionName) {
$overrideLabels = static::loadTypoScriptLabels($extensionName);
if ($value === null && isset($overrideLabels[$languageKey])) {
$value = '';
}
}
if ($value === null && (!empty(self::$LOCAL_LANG[$languageFilePath]['default'][$key][0]['target'])
|| isset(self::$LOCAL_LANG_UNSET[$languageFilePath]['default'][$key]))
) {
// Default language translation for key exists
// No charset conversion because default is English and thereby ASCII
$value = self::$LOCAL_LANG[$languageFilePath]['default'][$key][0]['target'];
}
if (is_array($arguments) && $arguments !== [] && $value !== null) {
// This unrolls arguments from $arguments - instead of calling vsprintf which receives arguments as an array.
......@@ -139,31 +103,32 @@ class LocalizationUtility
*
* @param string[] $alternativeLanguageKeys
*/
protected static function initializeLocalization(string $languageFilePath, string $languageKey, array $alternativeLanguageKeys, string $extensionName = null): void
protected static function initializeLocalization(string $languageFilePath, string $languageKey, array $alternativeLanguageKeys, ?string $extensionName): LanguageService
{
$languageFactory = GeneralUtility::makeInstance(LocalizationFactory::class);
if (empty(self::$LOCAL_LANG[$languageFilePath][$languageKey])) {
$parsedData = $languageFactory->getParsedData($languageFilePath, $languageKey);
foreach ($parsedData as $tempLanguageKey => $data) {
if (!empty($data)) {
self::$LOCAL_LANG[$languageFilePath][$tempLanguageKey] = $data;
}
$languageService = self::buildLanguageService($languageKey, $alternativeLanguageKeys, $languageFilePath);
if (!empty($extensionName)) {
$overrideLabels = static::loadTypoScriptLabels($extensionName);
if ($overrideLabels !== []) {
$languageService->overrideLabels($languageFilePath, $overrideLabels);
}
}
if ($languageKey !== 'default') {
foreach ($alternativeLanguageKeys as $alternativeLanguageKey) {
if (empty(self::$LOCAL_LANG[$languageFilePath][$alternativeLanguageKey])) {
$tempLL = $languageFactory->getParsedData($languageFilePath, $alternativeLanguageKey);
if (isset($tempLL[$alternativeLanguageKey])) {
self::$LOCAL_LANG[$languageFilePath][$alternativeLanguageKey] = $tempLL[$alternativeLanguageKey];
}
}
return $languageService;
}
protected static function buildLanguageService(string $languageKey, array $alternativeLanguageKeys, $languageFilePath): LanguageService
{
$languageKeyHash = sha1(json_encode(array_merge([$languageKey], $alternativeLanguageKeys, [$languageFilePath])));
$cache = self::getRuntimeCache();
if (!$cache->get($languageKeyHash)) {
$languageService = GeneralUtility::makeInstance(LanguageServiceFactory::class)->create($languageKey);
$languageService->init($languageKey);
if ($alternativeLanguageKeys !== []) {
$languageService->setDependencies($alternativeLanguageKeys);
}
$languageService->includeLLFile($languageFilePath);
$cache->set($languageKeyHash, $languageService);
}
if (!empty($extensionName)) {
static::loadTypoScriptLabels($extensionName, $languageFilePath);
}
return $cache->get($languageKeyHash);
}
/**
......@@ -175,14 +140,10 @@ class LocalizationUtility
}
/**
* Sets the currently active language keys.
* Resolves the currently active language key.
*/
protected static function getLanguageKeys(): array
protected static function getLanguageKey(): string
{
$languageKeys = [
'languageKey' => 'default',
'alternativeLanguageKeys' => [],
];
if (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
&& ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend()
) {
......@@ -191,56 +152,45 @@ class LocalizationUtility
// Get values from site language
if ($siteLanguage !== null) {
$languageKeys['languageKey'] = $siteLanguage->getTypo3Language();
}
$locales = GeneralUtility::makeInstance(Locales::class);
if ($locales->isValidLanguageKey($languageKeys['languageKey'])) {
$languageKeys['alternativeLanguageKeys'] = $locales->getLocaleDependencies($languageKeys['languageKey']);
$languageKeys['alternativeLanguageKeys'] = array_reverse($languageKeys['alternativeLanguageKeys']);
return $siteLanguage->getTypo3Language();
}
} elseif (!empty($GLOBALS['BE_USER']->user['lang'])) {
$languageKeys['languageKey'] = $GLOBALS['BE_USER']->user['lang'];
return $GLOBALS['BE_USER']->user['lang'];
} elseif (!empty(static::getLanguageService()->lang)) {
$languageKeys['languageKey'] = static::getLanguageService()->lang;
return static::getLanguageService()->lang;
}
return $languageKeys;
return 'default';
}
/**
* Overwrites labels that are set via TypoScript.
* TS locallang labels have to be configured like:
* plugin.tx_myextension._LOCAL_LANG.languageKey.key = value
* TS labels have to be configured like:
* plugin.tx_myextension._LOCAL_LANG.languageKey.key = value
*/
protected static function loadTypoScriptLabels(string $extensionName, string $languageFilePath): void
protected static function loadTypoScriptLabels(string $extensionName): array
{
$configurationManager = static::getConfigurationManager();
$frameworkConfiguration = $configurationManager->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FRAMEWORK, $extensionName);
if (!is_array($frameworkConfiguration['_LOCAL_LANG'] ?? false)) {
return;
return [];
}
self::$LOCAL_LANG_UNSET[$languageFilePath] = [];
$finalLabels = [];
foreach ($frameworkConfiguration['_LOCAL_LANG'] as $languageKey => $labels) {
if (!is_array($labels)) {
continue;
}
foreach ($labels as $labelKey => $labelValue) {
if (is_string($labelValue)) {
self::$LOCAL_LANG[$languageFilePath][$languageKey][$labelKey][0]['target'] = $labelValue;
if ($labelValue === '') {
self::$LOCAL_LANG_UNSET[$languageFilePath][$languageKey][$labelKey] = '';
}
$finalLabels[$languageKey][$labelKey] = $labelValue;
} elseif (is_array($labelValue)) {
$labelValue = self::flattenTypoScriptLabelArray($labelValue, $labelKey);
foreach ($labelValue as $key => $value) {
self::$LOCAL_LANG[$languageFilePath][$languageKey][$key][0]['target'] = $value;
if ($value === '') {
self::$LOCAL_LANG_UNSET[$languageFilePath][$languageKey][$key] = '';
}
$finalLabels[$languageKey][$key] = $value;
}
}
}
}
return $finalLabels;
}
/**
......@@ -303,4 +253,9 @@ class LocalizationUtility
{
return $GLOBALS['LANG'];
}
protected static function getRuntimeCache(): FrontendInterface
{
return GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
}
}
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="da" datatype="plaintext" original="EXT:label_test/Resources/Private/Language/locallang.xlf" date="2023-01-03T15:50:00Z" product-name="label_test">
<header/>
<body>
<trans-unit id="key1" resname="key1" approved="yes">
<source>English label for key1</source>
<target>Dansk label for key1</target>
</trans-unit>
<trans-unit id="key4" resname="key4" approved="yes">
<source>English label for key5</source>
<target></target>
</trans-unit>
<trans-unit id="key5" resname="key5" approved="yes">
<source>English label for key5</source>
<target></target>
</trans-unit>
<trans-unit id="keyWithPlaceholderAndNoArguments" resname="keyWithPlaceholder" approved="yes">
<source>%d/%m/%Y</source>
<target>%d-%m-%Y</target>
</trans-unit>
</body>
</file>
</xliff>
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="da_alt" datatype="plaintext" original="EXT:label_test/Resources/Private/Language/locallang.xlf" date="2023-01-03T15:50:00Z" product-name="label_test">
<header/>
<body>
<trans-unit id="key2" resname="key2" approved="yes">
<source>English label for key2</source>
<target>Dansk alternative label for key2</target>
</trans-unit>
<trans-unit id="key4" resname="key4" approved="yes">
<source>English label for key5</source>
<target></target>
</trans-unit>
<trans-unit id="key5" resname="key5" approved="yes">
<source>English label for key5</source>
<target>Dansk alternative label for key5</target>
</trans-unit>
</body>
</file>
</xliff>
<?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:label_test/Resources/Private/Language/locallang.xlf" date="2023-01-03T15:50:00Z" product-name="label_test">
<header/>
<body>
<trans-unit id="key1" resname="key1">
<source>English label for key1</source>
</trans-unit>
<trans-unit id="key2" resname="key2">
<source>English label for key2</source>
</trans-unit>
<trans-unit id="key3" resname="key3">
<source>English label for key3</source>
</trans-unit>
<trans-unit id="key3.subkey1" resname="key3.subkey1">
<source>English label for key3.subkey1</source>
</trans-unit>
<trans-unit id="key4" resname="key4">
<source>English label for key4</source>
</trans-unit>
<trans-unit id="keyWithPlaceholder" resname="keyWithPlaceholder">
<source>English label with number %d</source>
</trans-unit>
<trans-unit id="keyWithPlaceholderAndNoArguments" resname="keyWithPlaceholderAndNoArguments">
<source>%d/%m/%Y</source>
</trans-unit>
</body>
</file>
</xliff>
<?php
declare(strict_types=1);
$EM_CONF[$_EXTKEY] = [
'title' => '',
'description' => '',
'category' => 'example',
'author' => 'TYPO3 core team',
'author_company' => '',
'author_email' => '',
'state' => 'stable',
'version' => '12.2.0',
'constraints' => [
'depends' => [
'typo3' => '12.2.0',
],
'conflicts' => [],
'suggests' => [],
],
];
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment