packageManager->getPackageKeyFromComposerName($packageKey); $cacheIdentifier = 'translation-domains-of-package-' . $packageKey; $allLabelFilesOfPackage = $this->labelCache->get($cacheIdentifier); if (is_array($allLabelFilesOfPackage)) { return $allLabelFilesOfPackage; } $allLabelFilesOfPackage = $this->labelFileResolver->getAllLabelFilesOfPackage($packageKey); $domains = []; $domainPriorities = []; $package = $this->packageManager->getPackage($packageKey); foreach ($allLabelFilesOfPackage['default'] ?? [] as $file) { // Make path relative $file = $this->getRelativePath($file, $packageKey, $package); $domain = $this->translationDomainResolver->mapFileNameToDomain($file); $priority = $this->getFilePriority($file); // Only set/overwrite if this file has higher or equal priority if (!isset($domains[$domain]) || $priority >= $domainPriorities[$domain]) { $domains[$domain] = $file; $domainPriorities[$domain] = $priority; } } $event = new BeforeLabelResourceResolvedEvent( $packageKey, $domains ); $event = $this->eventDispatcher->dispatch($event); $this->labelCache->set($cacheIdentifier, $event->domains); return $event->domains; } /** * Find label resources in a package grouped by locale. * * Returns an array where keys are locale codes and values are arrays of domain => resource mappings. * Applies the same precedence rules as findLabelResourcesInPackage(). * * @return array> Array grouped by locale, then domain => resource */ public function findLabelResourcesInPackageGroupedByLocale(string $packageKey): array { $packageKey = $this->packageManager->getPackageKeyFromComposerName($packageKey); $package = $this->packageManager->getPackage($packageKey); $allLabelFilesOfPackage = $this->labelFileResolver->getAllLabelFilesOfPackage($packageKey); $domainsByLocale = []; $prioritiesByLocale = []; foreach ($allLabelFilesOfPackage as $locale => $files) { $domainsByLocale[$locale] = []; $prioritiesByLocale[$locale] = []; foreach ($files as $file) { $file = $this->getRelativePath($file, $packageKey, $package); $domain = $this->translationDomainResolver->mapFileNameToDomain($file); $priority = $this->getFilePriority($file); // Only set/overwrite if this file has higher or equal priority if (!isset($domainsByLocale[$locale][$domain]) || $priority >= $prioritiesByLocale[$locale][$domain]) { $domainsByLocale[$locale][$domain] = $file; $prioritiesByLocale[$locale][$domain] = $priority; } } } return $domainsByLocale; } /** * Determine file priority for domain collision resolution. * * Higher priority wins when multiple files map to the same domain. * * Priority levels: * - 3: Files without "locallang" prefix (messages.xlf, tabs.xlf) * - 2: Files with "locallang_" prefix (locallang_toolbar.xlf) * - 1: Plain locallang.xlf * * This ensures modern naming (messages.xlf) takes precedence over legacy (locallang.xlf). */ protected function getFilePriority(string $filePath): int { $fileName = basename($filePath); $fileNameWithoutExtension = $this->translationDomainResolver->getFileReferenceWithoutExtension($fileName); // Remove locale prefix if present (e.g., "de.locallang.xlf" -> "locallang.xlf") $locale = $this->translationDomainResolver->getLocaleFromLanguageFile($fileName); if ($locale !== null) { $fileNameWithoutExtension = substr($fileNameWithoutExtension, strlen($locale) + 1); } // Priority 3: Files without "locallang" prefix (e.g., messages.xlf, tabs.xlf) if (!str_contains($fileNameWithoutExtension, 'locallang')) { return 3; } // Priority 2: Files with "locallang_" prefix (e.g., locallang_toolbar.xlf) if (str_starts_with($fileNameWithoutExtension, 'locallang_')) { return 2; } // Priority 1: Plain locallang.xlf return 1; } /** * Maps a translation domain to a label resource file reference. * * Examples: * - "core.messages" -> "EXT:core/Resources/Private/Language/locallang.xlf" * - "backend.toolbar" -> "EXT:backend/Resources/Private/Language/locallang_toolbar.xlf" * - "core.form.tabs" -> "EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf" * - "felogin.sets.felogin" -> "EXT:felogin/Configuration/Sets/Felogin/labels.xlf" * * Invalid domain names return their values directly. * 1. More than one ":" included * 2. Domain Name contains other than [a-z0-9_.] characters */ public function mapDomainToFileName(string $domain): string { // In order to be deterministic, we need to find the proper file name to reference // now. It IS possible that the incoming domain is actually valid, so we skip it. // If it's already an EXT: reference or absolute path, return as-is if (str_starts_with($domain, 'EXT:') || GeneralUtility::isAllowedAbsPath($domain)) { return $domain; } if (!$this->translationDomainResolver->isValidDomainName($domain)) { return $domain; } // Parse domain into extension key part and resource part [$extensionKey, $resourcePart] = explode('.', $domain, 2); try { $allDomainsInPackage = $this->findLabelResourcesInPackage($extensionKey); } catch (\InvalidArgumentException) { return $domain; } // Fall back to domain, in case it's just a file reference. return $allDomainsInPackage[$domain] ?? $domain; } protected function getRelativePath(string $file, string $packageKey, PackageInterface $package): string { if (str_starts_with($file, $package->getPackagePath())) { $file = substr($file, strlen($package->getPackagePath())); $file = 'EXT:' . $packageKey . '/' . $file; } elseif (str_starts_with($file, Environment::getProjectPath())) { $file = substr($file, strlen(Environment::getProjectPath()) + 1); } return $file; } }