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 = 'serializeDispatcherAttributes() ?? 'disabled="true"')
. ' class="btn btn-default" title="' . $lang->sL('LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_viewPage') . '">'
. $this->iconFactory->getIcon('actions-view-page', IconSize::SMALL)->render() . ' ';
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 = 'withLanguage($languageId)
->serializeDispatcherAttributes() ?? 'disabled="true"')
. ' class="btn btn-default" title="' . $lang->sL('LLL:EXT:info/Resources/Private/Language/locallang_webinfo.xlf:lang_renderl10n_viewTranslatedPage') . '">'
. $this->iconFactory->getIcon('actions-view', IconSize::SMALL)->render() . ' ';
$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 = ''
. ' '
. ''
. ''
. '' . $this->iconFactory->getIcon('actions-check', IconSize::SMALL)->render() . ' '
. '' . $this->iconFactory->getIcon('empty-empty', IconSize::SMALL)->render() . ' '
. ' '
. ' '
. '
';
$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'];
}
}