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 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; } }