Files

350 lines
14 KiB
PHP

<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Localization;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Localization\Exception\FileNotFoundException;
use TYPO3\CMS\Core\Package\Exception\UnknownPackageException;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
/**
* Service class for resolving label file paths and determining loading order.
*
* This class handles:
* - Path resolving for localization files
* - File name resolving with language variants
* - Detecting resource override files via $GLOBALS['TYPO3_CONF_VARS']['LANG']['resourceOverrides']
* - Determining file loading order without merging content
*
* This class does not handle reading of file contents, and also returns the full path,
* so it does not care about caching. Should be handled at a more outer stage.
*
* @internal not part of TYPO3's public API.
*/
#[Autoconfigure(public: true)]
readonly class LabelFileResolver
{
public function __construct(
protected PackageManager $packageManager,
protected TranslationDomainResolver $translationDomainResolver,
) {}
/**
* Find all label files in a package, but also find overrides.
* All files are returned in the order they should be loaded.
*/
public function getAllLabelFilesOfPackage(string $packageKey, $defaultLocaleOnlyForCacheWarmup = false): array
{
$result = [];
try {
$packagePath = $this->packageManager->getPackage($packageKey)->getPackagePath();
} catch (UnknownPackageException) {
throw new \InvalidArgumentException(sprintf('Package with key "%s" not found', $packageKey), 1760479988);
}
$directoriesToSearch = [
'Resources/Private/Language/',
'Configuration/Sets/',
];
$allowedFileExtensions = $this->getSupportedExtensions();
$allowedFileExtensions = implode(',', $allowedFileExtensions);
foreach ($directoriesToSearch as $searchPath) {
$searchPath = $packagePath . $searchPath;
$files = GeneralUtility::getAllFilesAndFoldersInPath([], $searchPath, $allowedFileExtensions);
foreach ($files as $file) {
$fileName = PathUtility::basename($file);
$locale = $this->translationDomainResolver->getLocaleFromLanguageFile($fileName);
if ($locale === null) {
$locale = 'default';
}
if ($defaultLocaleOnlyForCacheWarmup && $locale !== 'default') {
continue;
}
$relativeFilePath = substr($file, strlen($packagePath));
$fileReference = 'EXT:' . $packageKey . '/' . $relativeFilePath;
if ($defaultLocaleOnlyForCacheWarmup) {
$result[$locale][] = $fileReference;
continue;
}
try {
$orderedFiles = $this->getOrderedFileResources($fileReference, $locale);
if ($orderedFiles !== []) {
if (!isset($result[$locale])) {
$result[$locale] = [];
}
$result[$locale] = array_merge($result[$locale], $orderedFiles);
}
} catch (FileNotFoundException $e) {
}
}
}
return $result;
}
/**
* Finds the actual files needed to resolve a file resource.
* This should be used later-on directly in LocalizationFactory.
*/
protected function getOrderedFileResources(string $fileReference, string $locale): array
{
$result = [];
try {
$baseFile = $this->resolveFileReference($fileReference, $locale);
if ($baseFile !== null) {
$result[] = $baseFile;
}
} catch (FileNotFoundException $e) {
}
$overrideFiles = $this->getOverrideFilePaths($fileReference, $locale);
if ($overrideFiles !== []) {
$result = array_merge($result, $overrideFiles);
}
return $result;
}
/**
* @throws FileNotFoundException
*/
public function resolveFileReference(string $fileReference, string $locale): ?string
{
$actualSourcePath = $this->getAbsoluteFileReference($fileReference);
if (PathUtility::isExtensionPath($fileReference)) {
$actualSourcePath = $this->resolveExtensionResourcePath($actualSourcePath, $locale, $fileReference);
}
if ($locale === 'default') {
// The "default" (=base) locale must not contain other language entries.
// Otherwise, when checking for a base locale file, it will hold an array of ALL
// other language variants, and then due to alphabetical sorting, any language with a
// first character AFTER "l" (locallang) would be regarded as the base entry.
return $actualSourcePath;
}
// Find localized file. If no localized version exists, return null.
$localizedSourcePath = $this->resolveLocalizedFilePath($actualSourcePath, $locale);
return $localizedSourcePath;
}
/**
* Get override file paths for localization
*
* This method returns an array of override file paths that should be loaded
* for the given file reference and language key. It supports both file path syntax
* (e.g., 'EXT:core/Resources/Private/Language/locallang.xlf') and domain syntax
* (e.g., 'core.messages').
*
* @return array<string> Array of absolute file paths to override files
*/
public function getOverrideFilePaths(string $fileReference, string $locale): array
{
if (!isset($GLOBALS['TYPO3_CONF_VARS']['LANG']['resourceOverrides'])) {
return [];
}
$validOverrideFiles = [];
$fileReferenceWithoutExtension = $this->getFileReferenceWithoutExtension($fileReference);
$overrideFiles = $GLOBALS['TYPO3_CONF_VARS']['LANG']['resourceOverrides'];
$supportedExtensions = $this->getSupportedExtensions();
// Build list of keys to check: file paths (with various extensions)
$keysToCheck = [];
foreach ($supportedExtensions as $extension) {
$keysToCheck[] = $fileReferenceWithoutExtension . '.' . $extension;
}
// Also add domain key for domain-syntax override support
$domain = $this->translationDomainResolver->mapFileNameToDomain($fileReference);
if ($domain !== $fileReference && $this->translationDomainResolver->isValidDomainName($domain)) {
$keysToCheck[] = $domain;
}
foreach ($keysToCheck as $key) {
// Check language-specific overrides first
if (isset($overrideFiles[$locale][$key]) && is_array($overrideFiles[$locale][$key])) {
$validOverrideFiles = array_merge($validOverrideFiles, $overrideFiles[$locale][$key]);
}
// Check general overrides (applies to all languages)
elseif (isset($overrideFiles[$key]) && is_array($overrideFiles[$key])) {
$validOverrideFiles = array_merge($validOverrideFiles, $overrideFiles[$key]);
}
}
$validOverrideFiles = array_unique($validOverrideFiles);
// Convert relative paths to absolute paths
$absoluteOverrideFiles = [];
foreach ($validOverrideFiles as $overrideFile) {
if (PathUtility::isExtensionPath($overrideFile)) {
$absoluteOverrideFiles[] = $overrideFile;
} else {
$absolutePath = GeneralUtility::getFileAbsFileName($overrideFile);
if ($absolutePath) {
$absoluteOverrideFiles[] = $absolutePath;
}
}
}
$absoluteOverrideFiles = array_unique($absoluteOverrideFiles);
return $absoluteOverrideFiles;
}
/**
* Get absolute file reference
*
* @throws FileNotFoundException Source localization file not found.
*/
protected function getAbsoluteFileReference(string $fileReference): string
{
$fileReferenceWithoutExtension = $this->getFileReferenceWithoutExtension($fileReference);
$supportedExtensions = $this->getSupportedExtensions();
foreach ($supportedExtensions as $extension) {
$fullPath = GeneralUtility::getFileAbsFileName($fileReferenceWithoutExtension . '.' . $extension);
if (@is_file($fullPath)) {
return $fullPath;
}
}
throw new FileNotFoundException(sprintf('Source localization file (%s) not found', $fileReference), 1306410755);
}
public function getFileReferenceWithoutExtension(string $fileReference): string
{
return $this->translationDomainResolver->getFileReferenceWithoutExtension($fileReference);
}
/**
* Get localized labels path pattern for extensions
*/
protected function getLocalizedLabelsPathPattern(string $fileReference): string
{
if (!PathUtility::isExtensionPath($fileReference)) {
throw new \InvalidArgumentException(sprintf('Invalid file reference configuration for the current file (%s)', $fileReference), 1635863703);
}
$packageKey = $this->packageManager->extractPackageKeyFromPackagePath($fileReference);
$relativeFileName = substr($fileReference, strlen($packageKey) + 5);
$directory = dirname($relativeFileName);
$fileName = basename($relativeFileName);
return sprintf(
'/%%1$s/%s/%s%%1$s.%s',
$packageKey,
($directory !== '.' ? $directory . '/' : ''),
$fileName
);
}
protected function getSupportedExtensions(): array
{
if (isset($GLOBALS['TYPO3_CONF_VARS']['LANG']['format']['priority']) && trim($GLOBALS['TYPO3_CONF_VARS']['LANG']['format']['priority']) !== '') {
return GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['LANG']['format']['priority']);
}
return ['xlf'];
}
protected function resolveExtensionResourcePath(string $sourcePath, string $locale, string $fileReference): string
{
$localizedLabelsPathPattern = $this->getLocalizedLabelsPathPattern($fileReference);
$fileName = Environment::getLabelsPath() . sprintf($localizedLabelsPathPattern, $locale);
if (@is_file($fileName)) {
return $fileName;
}
// Fallback to source path if localized version doesn't exist
return $sourcePath;
}
/**
* Resolve localized file path by trying multiple location strategies.
*
* This method attempts to find localized versions of files in the following order:
* 1. Check if the file already has the correct locale prefix (early return)
* 2. Try same directory with locale prefix: "de.locallang.xlf" in same folder
* 3. Try TYPO3 labels directory structure: "/var/labels/de/extension_key/path/de.filename.xlf"
*
* Language variant handling:
* - Supports both underscore and hyphen variants: "de_CH" <-> "de-CH"
* - Tests both formats when resolving files
*
* Examples:
* - Source: "/ext/core/Resources/Private/Language/locallang.xlf"
* - For locale "de": tries "de.locallang.xlf" in same dir, then "/var/labels/de/core/Resources/Private/Language/de.locallang.xlf"
* - For locale "de-CH": tries both "de-CH.locallang.xlf" and "de_CH.locallang.xlf" variants
*
* @param string $sourcePath Absolute path to the source localization file
* @param string $locale Language locale (e.g., "de", "de-CH", "de_AT")
* @return ?string Absolute path to the localized file, or null if no localized version found
*/
protected function resolveLocalizedFilePath(string $sourcePath, string $locale): ?string
{
$possiblePrefixes = [$locale];
if (str_contains($locale, '_')) {
$possiblePrefixes[] = str_replace('_', '-', $locale);
} elseif (str_contains($locale, '-')) {
$possiblePrefixes[] = str_replace('-', '_', $locale);
}
$packageRootPaths = [];
foreach ($this->packageManager->getActivePackages() as $package) {
$packageRootPaths[$package->getPackageKey()] = $package->getPackagePath();
}
foreach ($possiblePrefixes as $fileNamePrefix) {
$fileName = PathUtility::basename($sourcePath);
if (str_starts_with($fileName, $fileNamePrefix . '.')) {
return $sourcePath;
}
// Try same location first
$sameLocationPath = str_replace($fileName, $fileNamePrefix . '.' . $fileName, $sourcePath);
if (@is_file($sameLocationPath)) {
return $sameLocationPath;
}
// Try labels directory structure
$relativePathInPackagePath = '';
$extensionKey = null;
foreach ($packageRootPaths as $packageKey => $packageRootPath) {
if (str_starts_with($sourcePath, $packageRootPath)) {
$relativePathInPackagePath = substr($sourcePath, strlen($packageRootPath));
$extensionKey = $packageKey;
break;
}
}
if ($relativePathInPackagePath === '' || $extensionKey === null) {
continue;
}
[$relativePathInPackagePath, $baseName] = GeneralUtility::revExplode('/', $relativePathInPackagePath, 2);
$localizedPath = Environment::getLabelsPath() . '/' . $fileNamePrefix . '/' . $extensionKey . '/' . ($relativePathInPackagePath ? $relativePathInPackagePath . '/' : '') . $fileNamePrefix . '.' . $baseName;
if (@is_file($localizedPath)) {
return $localizedPath;
}
}
return null;
}
}