Localization overview * * @internal This class is a specific Backend controller implementation and is not part of the TYPO3's Core API. */ #[AsController] readonly class TranslationStatusController { public function __construct( private IconFactory $iconFactory, private UriBuilder $uriBuilder, private ModuleProvider $moduleProvider, private ModuleTemplateFactory $moduleTemplateFactory, private EventDispatcherInterface $eventDispatcher, private TcaSchemaFactory $tcaSchemaFactory, private ComponentFactory $componentFactory, private ConnectionPool $connectionPool, ) {} public function handleRequest(ServerRequestInterface $request): ResponseInterface { $backendUser = $this->getBackendUser(); $languageService = $this->getLanguageService(); $module = $request->getAttribute('module'); $moduleData = $request->getAttribute('moduleData'); $currentSite = $request->getAttribute('site'); $pageId = (int)($request->getQueryParams()['id'] ?? $request->getParsedBody()['id'] ?? 0); $pageinfo = BackendUtility::readPageAccess($pageId, $backendUser->getPagePermsClause(Permission::PAGE_SHOW)) ?: []; $hasAccess = false; if (($pageId && $pageinfo !== []) || ($backendUser->isAdmin() && $pageId === 0)) { $hasAccess = true; } if ($pageId === 0 && $backendUser->isAdmin()) { $pageinfo = ['title' => '[root-level]', 'uid' => 0, 'pid' => 0]; } $siteLanguages = $currentSite->getAvailableLanguages($backendUser, false, $pageId); $allowedModuleOptions = $this->getModuleOptions($siteLanguages); if ($moduleData->cleanUp($allowedModuleOptions)) { $backendUser->pushModuleData($moduleData->getModuleIdentifier(), $moduleData->toArray()); } $selectedDepth = (int)$moduleData->get('depth'); $selectedLanguage = (int)$moduleData->get('lang'); $mainContent = ''; if ($pageId > 0) { $tree = $this->getTree($pageId, $selectedDepth); $mainContent = $this->renderL10nTable($tree, $request, $siteLanguages, $selectedLanguage); } $view = $this->moduleTemplateFactory->create($request); $view->assign('hasAccess', $hasAccess); if ($hasAccess) { $view->setTitle($languageService->sL($module->getTitle()), $pageId !== 0 && isset($pageinfo['title']) ? $pageinfo['title'] : ''); $view->getDocHeaderComponent()->setPageBreadcrumb($pageinfo); $view->makeDocHeaderModuleMenu(['id' => $pageId]); $view->getDocHeaderComponent()->setShortcutContext($module->getIdentifier(), sprintf('%s [%d]', $languageService->sL($module->getTitle()), $pageId), ['id' => $pageId]); $previewUriBuilder = PreviewUriBuilder::create($pageinfo); if ($previewUriBuilder->isPreviewable()) { $previewDataAttributes = $previewUriBuilder ->withRootLine(BackendUtility::BEgetRootLine($pageinfo['uid'])) ->buildDispatcherDataAttributes(); $viewButton = $this->componentFactory->createLinkButton() ->setHref('#') ->setDataAttributes($previewDataAttributes ?? []) ->setDisabled(!$previewDataAttributes) ->setTitle($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage')) ->setIcon($this->iconFactory->getIcon('actions-view-page', IconSize::SMALL)) ->setShowLabelText(true); $view->addButtonToButtonBar($viewButton, ButtonBar::BUTTON_POSITION_LEFT, 2); } } $event = $this->eventDispatcher->dispatch(new ModifyInfoModuleContentEvent($hasAccess, $request, $module, $view)); if ($hasAccess) { $view->assignMultiple([ 'pageUid' => $pageId, 'depthDropdownOptions' => $allowedModuleOptions['depth'], 'depthDropdownCurrentValue' => $selectedDepth, 'langDropdownOptions' => $allowedModuleOptions['lang'], 'langDropdownCurrentValue' => $selectedLanguage, 'content' => $mainContent, 'headerContent' => $event->getHeaderContent(), 'footerContent' => $event->getFooterContent(), ]); } return $view->renderResponse('TranslationStatus'); } private function getModuleOptions(array $siteLanguages): array { $languageService = $this->getLanguageService(); $menuArray = [ 'depth' => [ 0 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_0'), 1 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_1'), 2 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_2'), 3 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_3'), 4 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_4'), 999 => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.depth_infi'), ], 'lang' => [], ]; foreach ($siteLanguages as $language) { if ($language->getLanguageId() === 0) { $menuArray['lang'][0] = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.allLanguages'); } else { $menuArray['lang'][$language->getLanguageId()] = $language->getTitle(); } } return $menuArray; } private function getTree(int $pageId, int $selectedDepth): PageTreeView { $tree = GeneralUtility::makeInstance(PageTreeView::class); $tree->init('AND ' . $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW)); $tree->tree[] = ['row' => BackendUtility::getRecordWSOL('pages', $pageId)]; // Create the tree from starting point if ($selectedDepth) { $tree->getTree($pageId, $selectedDepth); } return $tree; } /** * Rendering the localization information table. * * @param PageTreeView $tree The Page tree data * @return string HTML for the localization information table. */ private function renderL10nTable(PageTreeView $tree, ServerRequestInterface $request, array $siteLanguages, int $selectedLanguage): string { $lang = $this->getLanguageService(); $backendUser = $this->getBackendUser(); // Put together the TREE: $output = ''; $langRecUids = []; $userTsConfig = $backendUser->getTSConfig(); $showPageId = !empty($userTsConfig['options.']['pageTree.']['showPageIdWithTitle']); $pageModule = 'web_layout'; $pageModuleAccess = $this->moduleProvider->accessGranted($pageModule, $backendUser); foreach ($tree->tree as $data) { $tCells = []; $langRecUids[0][] = $data['row']['uid']; $pageTitle = ($showPageId ? '[' . (int)$data['row']['uid'] . '] ' : '') . $data['row']['title']; // Page icons / titles etc. if ($pageModuleAccess) { $pageModuleLink = (string)$this->uriBuilder->buildUriFromRoute($pageModule, ['id' => $data['row']['uid'], 'languages' => [0], 'viewMode' => PageViewMode::LayoutView->value]); $pageModuleLink = '' . htmlspecialchars($pageTitle) . ''; } else { $pageModuleLink = htmlspecialchars($pageTitle); } $icon = '' . $this->iconFactory->getIconForRecord('pages', $data['row'], IconSize::SMALL)->setTitle(BackendUtility::getRecordIconAltText($data['row'], 'pages', false))->render() . ''; if ($backendUser->checkRecordEditAccess('pages', $data['row'])->isAllowed) { $icon = BackendUtility::wrapClickMenuOnIcon($icon, 'pages', $data['row']['uid']); } $tCells[] = '' . '
' . (!empty($data['depthData']) ? $data['depthData'] : '') . ($data['HTML'] ?? '') . $icon . '' . $pageModuleLink . ((string)$data['row']['nav_title'] !== '' ? ' [Nav: ' . htmlspecialchars($data['row']['nav_title']) . ']' : '') . '' . '
' . ''; $previewUriBuilder = PreviewUriBuilder::create($data['row']); // DEFAULT language: $pageTranslationVisibility = new PageTranslationVisibility((int)($data['row']['l18n_cfg'] ?? 0)); $status = $pageTranslationVisibility->shouldBeHiddenInDefaultLanguage() ? 'danger' : 'success'; // Create links: $editUrl = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [ 'edit' => [ 'pages' => [ $data['row']['uid'] => 'edit', ], ], 'module' => 'web_info_translations', 'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(), ]); $info = ''; if ($backendUser->check('tables_modify', 'pages')) { $info .= '' . $this->iconFactory->getIcon('actions-page-open', IconSize::SMALL)->render() . ''; } $info .= ' '; $info .= $pageTranslationVisibility->shouldBeHiddenInDefaultLanguage() ? 'D' : ' '; $info .= $pageTranslationVisibility->shouldHideTranslationIfNoTranslatedRecordExists() ? 'N' : ' '; // Put into cell: $tCells[] = '
' . $info . '
'; $tCells[] = '' . ($this->getContentElementCount((int)$data['row']['uid'], 0) ?: '-') . ''; // Traverse system languages: foreach ($siteLanguages as $siteLanguage) { $languageId = $siteLanguage->getLanguageId(); if ($languageId === 0) { continue; } if ($selectedLanguage === 0 || $selectedLanguage === $languageId) { $row = $this->getLangStatus((int)$data['row']['uid'], $languageId); if ($pageTranslationVisibility->shouldBeHiddenInDefaultLanguage() || $pageTranslationVisibility->shouldHideTranslationIfNoTranslatedRecordExists()) { $status = 'danger'; } else { $status = ''; } if (is_array($row)) { $langRecUids[$languageId][] = $row['uid']; if (!$row['_HIDDEN']) { $status = 'success'; } if ($row['_COUNT'] > 1) { $status = 'warning'; } $info = ($showPageId ? ' [' . (int)$row['uid'] . '] ' : '') . htmlspecialchars($row['title']) . ((string)$row['nav_title'] !== '' ? ' [Nav: ' . htmlspecialchars($row['nav_title']) . ']' : '') . ($row['_COUNT'] > 1 ? '
' . $lang->sL('LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_badThingThereAre') . '
' : ''); if ($pageModuleAccess) { $pageModuleLink = (string)$this->uriBuilder->buildUriFromRoute($pageModule, ['id' => $data['row']['uid'], 'language' => [$languageId], 'viewMode' => PageViewMode::LanguageComparisonView->value]); $pageModuleLink = '' . $info . ''; } else { $pageModuleLink = $info; } $icon = '' . $this->iconFactory->getIconForRecord('pages', $row, IconSize::SMALL)->setTitle(BackendUtility::getRecordIconAltText($row, 'pages', false))->render() . ''; $tCells[] = '' . BackendUtility::wrapClickMenuOnIcon($icon, 'pages', (int)$row['uid']) . $pageModuleLink . ''; // Edit whole record: // Create links: $editUrl = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [ 'edit' => [ 'pages' => [ $row['uid'] => 'edit', ], ], 'module' => 'web_info_translations', 'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(), ]); // ViewPageLink $info = ''; $info .= '' . $this->iconFactory->getIcon('actions-open', IconSize::SMALL)->render() . ''; $tCells[] = '
' . $info . '
'; $tCells[] = '' . ($this->getContentElementCount((int)$data['row']['uid'], $languageId) ?: '-') . ''; } else { $idName = sprintf('new-overlay-%d-%d', $languageId, $data['row']['uid']); $info = '
' . '' . '' . '
'; $tCells[] = ' '; $tCells[] = ' '; $tCells[] = '' . $info . ''; } } } $output .= '' . implode('', $tCells) . ''; } // Put together HEADER: $headerCells = []; $headerCells[] = '' . $lang->sL('LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_page') . ''; if ($backendUser->check('tables_modify', 'pages')) { $editUrl = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [ 'edit' => [ 'pages' => [ implode(',', $langRecUids[0]) => 'edit', ], ], 'columnsOnly' => [ 'pages' => ['title', 'nav_title', 'l18n_cfg', 'hidden'], ], 'module' => 'web_info_translations', 'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(), ]); $editIco = '' . $this->iconFactory->getIcon('actions-document-open', IconSize::SMALL)->render() . ''; } else { $editIco = ''; } if (isset($siteLanguages[0])) { $defaultLanguageLabel = $siteLanguages[0]->getTitle(); } else { $defaultLanguageLabel = $lang->sL('LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_default'); } $headerCells[] = '' . htmlspecialchars($defaultLanguageLabel) . ' ' . $editIco . ''; foreach ($siteLanguages as $siteLanguage) { $languageId = $siteLanguage->getLanguageId(); if ($languageId === 0) { continue; } if ($selectedLanguage === 0 || $selectedLanguage === $languageId) { // Title: $headerCells[] = '' . htmlspecialchars($siteLanguage->getTitle()) . ''; // Edit language overlay records: if (is_array($langRecUids[$languageId] ?? null)) { $editUrl = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [ 'edit' => [ 'pages' => [ implode(',', $langRecUids[$languageId]) => 'edit', ], ], 'columnsOnly' => [ 'pages' => ['title', 'nav_title', 'hidden'], ], 'module' => 'web_info_translations', 'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri(), ]); $editButton = '' . $this->iconFactory->getIcon('actions-document-open', IconSize::SMALL)->render() . ''; } else { $editButton = ''; } // Create new overlay records: $createLink = (string)$this->uriBuilder->buildUriFromRoute('tce_db', [ 'redirect' => $request->getAttribute('normalizedParams')->getRequestUri(), ]); $newButton = '' . $this->iconFactory->getIcon('actions-document-new', IconSize::SMALL, null, IconState::STATE_DISABLED)->render() . ''; $headerCells[] = '' . $editButton . ''; $headerCells[] = '' . $newButton . ''; } } $output = '
' . '' . '' . '' . implode('', $headerCells) . '' . '' . '' . $output . '' . '
' . '
'; return $output; } /** * Get an alternative language record for a specific page / language * * @param int $pageId Page ID to look up for. * @param int $langId Language UID to select for. * @return array|bool translated pages record */ private function getLangStatus(int $pageId, int $langId): bool|array { $schema = $this->tcaSchemaFactory->get('pages'); /** @var LanguageAwareSchemaCapability $languageCapability */ $languageCapability = $schema->getCapability(TcaSchemaCapability::Language); $queryBuilder = $this->connectionPool->getQueryBuilderForTable('pages'); $queryBuilder ->getRestrictions() ->removeAll() ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace)) ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); $result = $queryBuilder ->select('*') ->from('pages') ->where( $queryBuilder->expr()->eq( $languageCapability->getTranslationOriginPointerField()->getName(), $queryBuilder->createNamedParameter($pageId, Connection::PARAM_INT) ) ) ->andWhere( $queryBuilder->expr()->eq( $languageCapability->getLanguageField()->getName(), $queryBuilder->createNamedParameter($langId, Connection::PARAM_INT) ) ) ->executeQuery(); $row = $result->fetchAssociative(); BackendUtility::workspaceOL('pages', $row); if (is_array($row)) { $row['_COUNT'] = $queryBuilder->count('uid')->executeQuery()->fetchOne(); $row['_HIDDEN'] = $row['hidden'] || (int)$row['endtime'] > 0 && (int)$row['endtime'] < $GLOBALS['EXEC_TIME'] || $GLOBALS['EXEC_TIME'] < (int)$row['starttime']; } $result->free(); return $row; } /** * Counting content elements for a single language on a page. * * @param int $pageId Page id to select for. * @param int $sysLang Sys language uid * @return int Number of content elements from the PID where the language is set to a certain value. */ private function getContentElementCount(int $pageId, int $sysLang): int { $queryBuilder = $this->connectionPool->getQueryBuilderForTable('tt_content'); $queryBuilder->getRestrictions() ->removeAll() ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace)); return (int)$queryBuilder ->count('uid') ->from('tt_content') ->where( $queryBuilder->expr()->eq( 'pid', $queryBuilder->createNamedParameter($pageId, Connection::PARAM_INT) ) ) ->andWhere( $queryBuilder->expr()->eq( 'sys_language_uid', $queryBuilder->createNamedParameter($sysLang, Connection::PARAM_INT) ) ) ->executeQuery() ->fetchOne(); } private function getLanguageService(): LanguageService { return $GLOBALS['LANG']; } private function getBackendUser(): BackendUserAuthentication { return $GLOBALS['BE_USER']; } }