780 lines
31 KiB
PHP
780 lines
31 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\Backend\Controller\Wizard;
|
|
|
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
|
use Psr\Http\Message\ResponseInterface;
|
|
use Psr\Http\Message\ServerRequestInterface;
|
|
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
|
use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
|
|
use TYPO3\CMS\Backend\Controller\Event\AfterPageColumnsSelectedForLocalizationEvent;
|
|
use TYPO3\CMS\Backend\Controller\Event\AfterRecordSummaryForLocalizationEvent;
|
|
use TYPO3\CMS\Backend\Domain\Repository\Localization\LocalizationRepository;
|
|
use TYPO3\CMS\Backend\Localization\LocalizationHandlerInterface;
|
|
use TYPO3\CMS\Backend\Localization\LocalizationHandlerRegistry;
|
|
use TYPO3\CMS\Backend\Localization\LocalizationInstructions;
|
|
use TYPO3\CMS\Backend\Localization\LocalizationMode;
|
|
use TYPO3\CMS\Backend\Localization\LocalizationResult;
|
|
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
|
use TYPO3\CMS\Backend\View\BackendLayoutView;
|
|
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
|
use TYPO3\CMS\Core\Database\Connection;
|
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
|
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
|
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
|
|
use TYPO3\CMS\Core\Http\JsonResponse;
|
|
use TYPO3\CMS\Core\Http\Response;
|
|
use TYPO3\CMS\Core\Imaging\IconFactory;
|
|
use TYPO3\CMS\Core\Imaging\IconSize;
|
|
use TYPO3\CMS\Core\Localization\LanguageService;
|
|
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
|
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
|
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
use TYPO3\CMS\Core\Versioning\VersionState;
|
|
|
|
/**
|
|
* LocalizationController handles the AJAX requests for the localization wizard.
|
|
*
|
|
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
|
*/
|
|
#[Autoconfigure(public: true)]
|
|
readonly class LocalizationController
|
|
{
|
|
public function __construct(
|
|
protected IconFactory $iconFactory,
|
|
protected LocalizationRepository $localizationRepository,
|
|
protected EventDispatcherInterface $eventDispatcher,
|
|
protected LocalizationHandlerInterface $localizationHandler,
|
|
protected LocalizationHandlerRegistry $localizationHandlerRegistry,
|
|
protected TcaSchemaFactory $schemaFactory,
|
|
protected TranslationConfigurationProvider $translationConfigurationProvider,
|
|
protected BackendLayoutView $backendLayoutView,
|
|
protected ConnectionPool $connectionPool,
|
|
) {}
|
|
|
|
/**
|
|
* Get record information for localization wizard
|
|
*/
|
|
public function getRecord(ServerRequestInterface $request): ResponseInterface
|
|
{
|
|
$params = $request->getQueryParams();
|
|
if (!isset($params['recordType'], $params['recordUid'])) {
|
|
return new JsonResponse(null, 400);
|
|
}
|
|
|
|
$recordType = $params['recordType'];
|
|
$recordUid = (int)$params['recordUid'];
|
|
|
|
$record = BackendUtility::getRecord($recordType, $recordUid);
|
|
if (!$record) {
|
|
return new JsonResponse(null, 404);
|
|
}
|
|
|
|
$schema = $this->schemaFactory->get($recordType);
|
|
$recordTitle = BackendUtility::getRecordTitle($recordType, $record);
|
|
$recordInfo = [
|
|
'uid' => $record['uid'],
|
|
'title' => BackendUtility::cropToTitleLength($recordTitle),
|
|
'icon' => $this->iconFactory->getIconForRecord($recordType, $record, IconSize::SMALL)->getIdentifier(),
|
|
'type' => $recordType,
|
|
'typeName' => $schema->getTitle($this->getLanguageService()->sL(...)),
|
|
];
|
|
|
|
return new JsonResponse($recordInfo);
|
|
}
|
|
|
|
/**
|
|
* Get available localization handlers
|
|
*
|
|
* Returns handlers filtered by the localization context
|
|
*/
|
|
public function getHandlers(ServerRequestInterface $request): ResponseInterface
|
|
{
|
|
$params = $request->getQueryParams();
|
|
|
|
try {
|
|
$localizationInstructions = LocalizationInstructions::create($params);
|
|
} catch (\ValueError) {
|
|
return new JsonResponse(['error' => 'Invalid localization mode'], 400);
|
|
} catch (\InvalidArgumentException) {
|
|
// Validate required parameters
|
|
return new JsonResponse(null, 400);
|
|
}
|
|
|
|
// Get available handlers from registry
|
|
$handlers = $this->localizationHandlerRegistry->getAvailableHandlers($localizationInstructions);
|
|
|
|
// Prepare handlers for JSON response with translated labels
|
|
$result = [];
|
|
foreach ($handlers as $handler) {
|
|
$result[] = [
|
|
'identifier' => $handler->getIdentifier(),
|
|
'label' => $this->getLanguageService()->sL($handler->getLabel()),
|
|
'description' => $this->getLanguageService()->sL($handler->getDescription()),
|
|
'iconIdentifier' => $handler->getIconIdentifier(),
|
|
];
|
|
}
|
|
|
|
return new JsonResponse($result);
|
|
}
|
|
|
|
/**
|
|
* Get available localization modes
|
|
*/
|
|
public function getModes(ServerRequestInterface $request): ResponseInterface
|
|
{
|
|
$params = $request->getQueryParams();
|
|
if (!isset($params['recordType'], $params['recordUid'], $params['targetLanguage'])) {
|
|
return new JsonResponse(null, 400);
|
|
}
|
|
|
|
$recordType = $params['recordType'];
|
|
$recordUid = (int)$params['recordUid'];
|
|
$targetLanguage = (int)$params['targetLanguage'];
|
|
|
|
// For pages, use the recordUid directly as the page
|
|
// For other record types, find the parent page
|
|
if ($recordType === 'pages') {
|
|
$page = $recordUid;
|
|
} else {
|
|
// Get the record to find its parent page
|
|
$record = BackendUtility::getRecord($recordType, $recordUid);
|
|
if (!$record) {
|
|
return new JsonResponse(null, 404);
|
|
}
|
|
$page = (int)$record['pid'];
|
|
}
|
|
|
|
// Get page record for permission checks
|
|
$pageRecord = BackendUtility::readPageAccess($page, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW));
|
|
if (!$pageRecord) {
|
|
return new JsonResponse(null, 403);
|
|
}
|
|
|
|
// Get available modes based on PageTSconfig
|
|
$pageTsConfig = BackendUtility::getPagesTSconfig($page);
|
|
$schema = $this->schemaFactory->get($recordType);
|
|
if (!$schema->hasCapability(TcaSchemaCapability::Language)) {
|
|
// Table is not language-aware
|
|
return new JsonResponse(null, 400);
|
|
}
|
|
$languageCapability = $schema->getCapability(TcaSchemaCapability::Language);
|
|
|
|
$availableModes = array_filter(
|
|
LocalizationMode::cases(),
|
|
static function (LocalizationMode $mode) use ($pageTsConfig, $languageCapability): bool {
|
|
return match ($mode) {
|
|
LocalizationMode::COPY => ($pageTsConfig['mod.']['web_layout.']['localization.']['enableCopy'] ?? true) && $languageCapability->hasTranslationSourceField(),
|
|
LocalizationMode::TRANSLATE => (bool)($pageTsConfig['mod.']['web_layout.']['localization.']['enableTranslate'] ?? true),
|
|
};
|
|
}
|
|
);
|
|
|
|
// Check if there are existing translations in the target language
|
|
// If so, we need to ensure we don't mix localization modes
|
|
// This check is only relevant for pages and tt_content records
|
|
if ($recordType === 'pages' || $recordType === 'tt_content') {
|
|
$existingMode = $this->detectExistingLocalizationMode($page, $targetLanguage);
|
|
if ($existingMode !== null) {
|
|
// Filter to only allow the existing mode
|
|
$availableModes = array_filter(
|
|
$availableModes,
|
|
static fn(LocalizationMode $mode): bool => $mode === $existingMode
|
|
);
|
|
}
|
|
}
|
|
|
|
// Sort by priority (highest first)
|
|
usort($availableModes, static fn(LocalizationMode $a, LocalizationMode $b): int => $b->getPriority() <=> $a->getPriority());
|
|
|
|
$modes = array_map(
|
|
fn(LocalizationMode $mode): array => [
|
|
'key' => $mode->value,
|
|
'label' => $this->getLanguageService()->sL($mode->getLabel()),
|
|
'description' => $this->getLanguageService()->sL($mode->getDescription()),
|
|
'iconIdentifier' => $mode->getIconIdentifier(),
|
|
],
|
|
$availableModes
|
|
);
|
|
|
|
return new JsonResponse($modes);
|
|
}
|
|
|
|
/**
|
|
* Get all target languages available for translation (excluding default language)
|
|
*/
|
|
public function getTargets(ServerRequestInterface $request): ResponseInterface
|
|
{
|
|
$params = $request->getQueryParams();
|
|
if (!isset($params['recordType'], $params['recordUid'])) {
|
|
return new JsonResponse(null, 400);
|
|
}
|
|
|
|
$recordType = $params['recordType'];
|
|
$recordUid = (int)$params['recordUid'];
|
|
|
|
// For pages, use the recordUid directly as the page
|
|
// For other record types, find the parent page
|
|
if ($recordType === 'pages') {
|
|
$page = $recordUid;
|
|
} else {
|
|
// Get the record to find its parent page
|
|
$record = BackendUtility::getRecord($recordType, $recordUid);
|
|
if (!$record) {
|
|
return new JsonResponse(null, 404);
|
|
}
|
|
$page = (int)$record['pid'];
|
|
}
|
|
|
|
// Get page record for permission checks
|
|
$pageRecord = BackendUtility::readPageAccess($page, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW));
|
|
if (!$pageRecord) {
|
|
return new JsonResponse(null, 403);
|
|
}
|
|
|
|
$systemLanguages = $this->translationConfigurationProvider->getSystemLanguages($page);
|
|
$availableLanguages = [];
|
|
foreach ($systemLanguages as $languageUid => $language) {
|
|
// Exclude "All languages" (-1) and default language (0) for target language selection
|
|
if ($languageUid !== -1 && $languageUid !== 0) {
|
|
$availableLanguages[] = $language;
|
|
}
|
|
}
|
|
|
|
return new JsonResponse($availableLanguages);
|
|
}
|
|
|
|
/**
|
|
* Get source languages that have content and can be used as translation base
|
|
*/
|
|
public function getSources(ServerRequestInterface $request): ResponseInterface
|
|
{
|
|
$params = $request->getQueryParams();
|
|
if (!isset($params['recordType'], $params['recordUid'], $params['targetLanguage'])) {
|
|
return new JsonResponse(null, 400);
|
|
}
|
|
|
|
$recordType = (string)$params['recordType'];
|
|
$recordUid = (int)$params['recordUid'];
|
|
$targetLanguage = (int)$params['targetLanguage'];
|
|
|
|
// For pages, use the recordUid directly as the page
|
|
// For other record types, find the parent page
|
|
if ($recordType === 'pages') {
|
|
$page = $recordUid;
|
|
} else {
|
|
// Get the record to find its parent page
|
|
$record = BackendUtility::getRecord($recordType, $recordUid);
|
|
if (!$record) {
|
|
return new JsonResponse(null, 404);
|
|
}
|
|
$page = (int)$record['pid'];
|
|
}
|
|
|
|
// Get page record for permission checks
|
|
$pageRecord = BackendUtility::readPageAccess($page, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW));
|
|
if (!$pageRecord) {
|
|
return new JsonResponse(null, 403);
|
|
}
|
|
|
|
$systemLanguages = $this->translationConfigurationProvider->getSystemLanguages($page);
|
|
$availableLanguages = [];
|
|
|
|
// Find all existing translations of the record
|
|
$record = BackendUtility::getRecord($recordType, $recordUid);
|
|
if ($record) {
|
|
$existingLanguageUids = [0]; // Always include default language
|
|
|
|
// Check each system language to see if a translation exists
|
|
foreach (array_keys($systemLanguages) as $languageUid) {
|
|
if ($languageUid > 0) { // Skip default language (0) and "All languages" (-1)
|
|
$translation = $this->localizationRepository->getRecordTranslation($recordType, $record, (int)$languageUid);
|
|
if ($translation !== null) {
|
|
$existingLanguageUids[] = $languageUid;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($existingLanguageUids as $languageUid) {
|
|
if ($languageUid !== $targetLanguage && isset($systemLanguages[$languageUid])) {
|
|
$availableLanguages[] = $systemLanguages[$languageUid];
|
|
}
|
|
}
|
|
|
|
// For pages with existing translations in the target language, we need to restrict source languages
|
|
// to prevent mixed translation origins (e.g., some content from language A, some from language B)
|
|
if ($recordType === 'pages') {
|
|
$availableLanguages = $this->filterSourceLanguagesForPage($recordUid, $targetLanguage, $availableLanguages);
|
|
}
|
|
}
|
|
|
|
// Language "All" should not appear as a source of translations (see bug 92757) and keys should be sequential
|
|
$availableLanguages = array_values(
|
|
array_filter($availableLanguages, static function (array $languageRecord): bool {
|
|
return (int)$languageRecord['uid'] !== -1;
|
|
})
|
|
);
|
|
|
|
return new JsonResponse($availableLanguages);
|
|
}
|
|
|
|
/**
|
|
* Get page layout and records for localization
|
|
*/
|
|
public function getContent(ServerRequestInterface $request): ResponseInterface
|
|
{
|
|
$params = $request->getQueryParams();
|
|
if (!isset($params['pageUid'], $params['targetLanguage'], $params['sourceLanguage'])) {
|
|
return new JsonResponse(null, 400);
|
|
}
|
|
|
|
$pageUid = (int)$params['pageUid'];
|
|
$targetLanguage = (int)$params['targetLanguage'];
|
|
$sourceLanguage = (int)$params['sourceLanguage'];
|
|
|
|
$records = [];
|
|
$result = $this->localizationRepository->getRecordsToCopyDatabaseResult(
|
|
$pageUid,
|
|
$targetLanguage,
|
|
$sourceLanguage,
|
|
$this->getBackendUser()->workspace
|
|
);
|
|
|
|
$flatRecords = [];
|
|
while ($row = $result->fetchAssociative()) {
|
|
BackendUtility::workspaceOL('tt_content', $row, $this->getBackendUser()->workspace, true);
|
|
if (!$row || VersionState::tryFrom($row['t3ver_state'] ?? 0) === VersionState::DELETE_PLACEHOLDER) {
|
|
continue;
|
|
}
|
|
$colPos = $row['colPos'];
|
|
|
|
if (!$this->backendLayoutView->isCTypeAllowedInColPosByPage($row['CType'], $colPos, $pageUid)) {
|
|
continue;
|
|
}
|
|
|
|
if (!isset($records[$colPos])) {
|
|
$records[$colPos] = [];
|
|
}
|
|
$recordTitle = BackendUtility::getRecordTitle('tt_content', $row);
|
|
$records[$colPos][] = [
|
|
'icon' => $this->iconFactory->getIconForRecord('tt_content', $row, IconSize::SMALL)->getIdentifier(),
|
|
'title' => BackendUtility::cropToTitleLength($recordTitle),
|
|
'uid' => $row['uid'],
|
|
];
|
|
$flatRecords[] = $row;
|
|
}
|
|
|
|
$columns = $this->getPageColumns($pageUid, $flatRecords, $params);
|
|
$event = new AfterRecordSummaryForLocalizationEvent($records, $columns);
|
|
$this->eventDispatcher->dispatch($event);
|
|
|
|
// Get the backend layout structure for visual representation
|
|
$backendLayout = $this->backendLayoutView->getBackendLayoutForPage($pageUid);
|
|
$layoutStructure = $this->buildLayoutStructure($backendLayout, $event->getColumns(), $event->getRecords());
|
|
|
|
return new JsonResponse([
|
|
'layout' => $layoutStructure,
|
|
]);
|
|
}
|
|
|
|
public function localize(ServerRequestInterface $request): ResponseInterface
|
|
{
|
|
$params = $request->getParsedBody();
|
|
|
|
if (!isset(
|
|
$params['recordType'],
|
|
$params['recordUid'],
|
|
$params['data']['sourceLanguage'],
|
|
$params['data']['targetLanguage'],
|
|
$params['data']['localizationMode']
|
|
)) {
|
|
return new JsonResponse(null, 400);
|
|
}
|
|
|
|
$recordType = $params['recordType'];
|
|
$recordUid = (int)$params['recordUid'];
|
|
$sourceLanguage = (int)$params['data']['sourceLanguage'];
|
|
$targetLanguage = (int)$params['data']['targetLanguage'];
|
|
$modeIdentifier = $params['data']['localizationMode'];
|
|
$handlerIdentifier = $params['data']['localizationHandler'] ?? 'manual';
|
|
|
|
// Prepare Additional Data
|
|
$additionalData = $params['data'];
|
|
unset($additionalData['sourceLanguage']);
|
|
unset($additionalData['targetLanguage']);
|
|
unset($additionalData['localizationMode']);
|
|
unset($additionalData['localizationHandler']);
|
|
|
|
// Validate that the mode exists
|
|
$mode = LocalizationMode::tryFrom($modeIdentifier);
|
|
if ($mode === null) {
|
|
$response = new Response('php://temp', 400, ['Content-Type' => 'application/json; charset=utf-8']);
|
|
$response->getBody()->write('Invalid localization mode "' . $modeIdentifier . '" called.');
|
|
return $response;
|
|
}
|
|
|
|
try {
|
|
$localizationInstructions = new LocalizationInstructions(
|
|
$recordType,
|
|
$recordUid,
|
|
$sourceLanguage,
|
|
$targetLanguage,
|
|
$mode,
|
|
$additionalData
|
|
);
|
|
} catch (\ValueError) {
|
|
return new JsonResponse(['error' => 'Invalid localization mode'], 400);
|
|
} catch (\InvalidArgumentException) {
|
|
// Validate required parameters
|
|
return new JsonResponse(null, 400);
|
|
}
|
|
|
|
// Process the localization using the handler
|
|
try {
|
|
// Get the handler from the registry or fall back to the default handler
|
|
if ($this->localizationHandlerRegistry->hasHandler($handlerIdentifier)) {
|
|
$handler = $this->localizationHandlerRegistry->getHandler($handlerIdentifier);
|
|
} else {
|
|
$handler = $this->localizationHandler;
|
|
}
|
|
|
|
// Use the handler to process the localization with the selected mode
|
|
$result = $handler->processLocalization($localizationInstructions);
|
|
} catch (\Exception $e) {
|
|
$result = LocalizationResult::error([$e->getMessage()]);
|
|
}
|
|
return new JsonResponse($result->jsonSerialize());
|
|
}
|
|
|
|
private function getPageColumns(int $page, array $flatRecords, array $params): array
|
|
{
|
|
$columns = [];
|
|
$backendLayout = $this->backendLayoutView->getBackendLayoutForPage($page);
|
|
|
|
foreach ($backendLayout->getUsedColumns() as $columnPos => $columnLabel) {
|
|
$columns[$columnPos] = $this->getLanguageService()->sL($columnLabel);
|
|
}
|
|
|
|
$event = new AfterPageColumnsSelectedForLocalizationEvent($columns, [], $backendLayout, $flatRecords, $params);
|
|
$this->eventDispatcher->dispatch($event);
|
|
|
|
return $event->getColumns();
|
|
}
|
|
|
|
private function buildLayoutStructure($backendLayout, array $columns, array $records): array
|
|
{
|
|
// Calculate total elements across all columns
|
|
$elementCount = 0;
|
|
foreach ($records as $colPos => $columnRecords) {
|
|
if (is_array($columnRecords)) {
|
|
$elementCount += count($columnRecords);
|
|
}
|
|
}
|
|
|
|
if (!$backendLayout) {
|
|
// Create a simple single-row layout when no backend layout is available
|
|
$layoutColumns = [];
|
|
foreach ($columns as $colPos => $columnLabel) {
|
|
$layoutColumns[] = [
|
|
'position' => (int)$colPos,
|
|
'label' => $columnLabel,
|
|
'records' => $records[$colPos] ?? [],
|
|
'colspan' => 1,
|
|
'rowspan' => 1,
|
|
'identifier' => null,
|
|
];
|
|
}
|
|
|
|
return [
|
|
'type' => 'layout',
|
|
'title' => 'Default Layout',
|
|
'identifier' => 'default',
|
|
'colCount' => count($columns),
|
|
'rowCount' => 1,
|
|
'elementCount' => $elementCount,
|
|
'rows' => [
|
|
[
|
|
'columns' => $layoutColumns,
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
$structure = $backendLayout->getStructure();
|
|
$layoutRows = [];
|
|
|
|
if (!empty($structure['__config']['backend_layout.']['rows.'])) {
|
|
$rows = $structure['__config']['backend_layout.']['rows.'];
|
|
ksort($rows);
|
|
|
|
foreach ($rows as $row) {
|
|
$layoutColumns = [];
|
|
|
|
if (!empty($row['columns.'])) {
|
|
foreach ($row['columns.'] as $column) {
|
|
if (!isset($column['colPos'])) {
|
|
continue;
|
|
}
|
|
|
|
$colPos = (int)$column['colPos'];
|
|
$layoutColumns[] = [
|
|
'position' => $colPos,
|
|
'label' => $columns[$colPos] ?? $column['name'],
|
|
'records' => $records[$colPos] ?? [],
|
|
'colspan' => (int)($column['colspan'] ?? 1),
|
|
'rowspan' => (int)($column['rowspan'] ?? 1),
|
|
'identifier' => $column['identifier'] ?? null,
|
|
];
|
|
}
|
|
}
|
|
|
|
$layoutRows[] = [
|
|
'columns' => $layoutColumns,
|
|
];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'type' => 'layout',
|
|
'title' => $backendLayout->getTitle(),
|
|
'identifier' => $backendLayout->getIdentifier(),
|
|
'colCount' => $backendLayout->getColCount(),
|
|
'rowCount' => $backendLayout->getRowCount(),
|
|
'elementCount' => $elementCount,
|
|
'rows' => $layoutRows,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Filter available source languages for page translations based on existing content
|
|
*
|
|
* For pages, when translations already exist in the target language with content assigned,
|
|
* we need to ensure that new content is only translated from the same source language(s)
|
|
* as the existing content to avoid creating mixed translations in terms of language origin.
|
|
*
|
|
* @param int $pageUid The page UID being translated
|
|
* @param int $targetLanguage The target language ID
|
|
* @param array $availableLanguages All available source languages (to be filtered)
|
|
* @return array Filtered available language configurations
|
|
*/
|
|
private function filterSourceLanguagesForPage(int $pageUid, int $targetLanguage, array $availableLanguages): array
|
|
{
|
|
// Check if a page translation exists in the target language
|
|
$pageTranslation = $this->localizationRepository->getPageTranslations($pageUid, [$targetLanguage], $this->getBackendUser()->workspace);
|
|
if ($pageTranslation === []) {
|
|
return $availableLanguages;
|
|
}
|
|
|
|
// Get source languages used by existing content in the target language
|
|
// Note: Content elements are stored on the original page with sys_language_uid set to the target language
|
|
$usedSourceLanguages = $this->getUsedSourceLanguagesForPage($pageUid, $targetLanguage);
|
|
|
|
if (empty($usedSourceLanguages)) {
|
|
return $availableLanguages;
|
|
}
|
|
|
|
// Filter to only allow source languages already in use
|
|
return array_filter(
|
|
$availableLanguages,
|
|
static fn(array $language): bool => isset($usedSourceLanguages[(int)$language['uid']])
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get the source languages used by existing content on a page
|
|
*
|
|
* @param int $pageUid The page UID
|
|
* @param int $targetLanguage The target language ID
|
|
* @return array<int, true> Map of source language UIDs that are in use
|
|
*/
|
|
private function getUsedSourceLanguagesForPage(int $pageUid, int $targetLanguage): array
|
|
{
|
|
$schema = $this->schemaFactory->get('tt_content');
|
|
$languageCapability = $schema->getCapability(TcaSchemaCapability::Language);
|
|
|
|
if (!$languageCapability->hasTranslationSourceField()) {
|
|
return [];
|
|
}
|
|
|
|
$languageField = $languageCapability->getLanguageField()->getName();
|
|
$translationSourceField = $languageCapability->getTranslationSourceField()->getName();
|
|
|
|
// Get all l10n_source UIDs from translated content
|
|
$queryBuilder = $this->connectionPool
|
|
->getQueryBuilderForTable('tt_content');
|
|
$queryBuilder->getRestrictions()
|
|
->removeAll()
|
|
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
|
|
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
|
|
|
|
$result = $queryBuilder
|
|
->select($translationSourceField)
|
|
->from('tt_content')
|
|
->where(
|
|
$queryBuilder->expr()->eq(
|
|
'pid',
|
|
$queryBuilder->createNamedParameter($pageUid, Connection::PARAM_INT)
|
|
),
|
|
$queryBuilder->expr()->eq(
|
|
$languageField,
|
|
$queryBuilder->createNamedParameter($targetLanguage, Connection::PARAM_INT)
|
|
),
|
|
$queryBuilder->expr()->gt(
|
|
$translationSourceField,
|
|
$queryBuilder->createNamedParameter(0, Connection::PARAM_INT)
|
|
)
|
|
)
|
|
->executeQuery();
|
|
|
|
// Collect unique source UIDs
|
|
$sourceUids = [];
|
|
while ($row = $result->fetchAssociative()) {
|
|
$sourceUid = (int)$row[$translationSourceField];
|
|
$sourceUids[$sourceUid] = $sourceUid;
|
|
}
|
|
|
|
if (empty($sourceUids)) {
|
|
return [];
|
|
}
|
|
|
|
// Get the language of all source records
|
|
$sourceQueryBuilder = $this->connectionPool
|
|
->getQueryBuilderForTable('tt_content');
|
|
$sourceQueryBuilder->getRestrictions()
|
|
->removeAll()
|
|
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
|
|
|
|
$sourceResult = $sourceQueryBuilder
|
|
->select($languageField)
|
|
->from('tt_content')
|
|
->where(
|
|
$sourceQueryBuilder->expr()->in(
|
|
'uid',
|
|
$sourceQueryBuilder->createNamedParameter($sourceUids, Connection::PARAM_INT_ARRAY)
|
|
)
|
|
)
|
|
->groupBy($languageField)
|
|
->executeQuery();
|
|
|
|
$usedSourceLanguages = [];
|
|
while ($row = $sourceResult->fetchAssociative()) {
|
|
$sourceLanguageUid = (int)$row[$languageField];
|
|
$usedSourceLanguages[$sourceLanguageUid] = true;
|
|
}
|
|
|
|
return $usedSourceLanguages;
|
|
}
|
|
|
|
/**
|
|
* Detect the localization mode used by existing translations on a page
|
|
*
|
|
* This method checks if there are existing content elements in the target language
|
|
* and determines whether they were created using COPY (free) or TRANSLATE (connected) mode.
|
|
* This prevents mixing different localization modes on the same page, which would lead to
|
|
* inconsistent translation workflows.
|
|
*
|
|
* Note: Pages themselves are always created in connected mode (using 'localize' command),
|
|
* so we check the content elements on the page to determine the actual localization mode.
|
|
*
|
|
* The distinction is made by checking the translation origin pointer field obtained from the
|
|
* schema's language capability:
|
|
* - TRANSLATE mode (connected): Records have translation origin pointer > 0 (linked to source language)
|
|
* - COPY mode (free): Records have translation origin pointer = 0 (independent copies)
|
|
*
|
|
* @param int $pageId The page ID to check
|
|
* @param int $targetLanguage The target language ID
|
|
* @return LocalizationMode|null The detected mode, or null if no translations exist
|
|
*/
|
|
private function detectExistingLocalizationMode(int $pageId, int $targetLanguage): ?LocalizationMode
|
|
{
|
|
// Get the TCA schema to determine the correct field names
|
|
$schema = $this->schemaFactory->get('tt_content');
|
|
|
|
if (!$schema->hasCapability(TcaSchemaCapability::Language)) {
|
|
// Table is not language-aware
|
|
return null;
|
|
}
|
|
|
|
$languageCapability = $schema->getCapability(TcaSchemaCapability::Language);
|
|
$transOrigPointerField = $languageCapability->getTranslationOriginPointerField()->getName();
|
|
$languageField = $languageCapability->getLanguageField()->getName();
|
|
|
|
// Check content elements on the page, as pages themselves are always connected
|
|
// but their content determines the actual localization mode being used
|
|
$queryBuilder = $this->connectionPool
|
|
->getQueryBuilderForTable('tt_content');
|
|
|
|
$queryBuilder->getRestrictions()
|
|
->removeAll()
|
|
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
|
|
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->getBackendUser()->workspace));
|
|
|
|
// Select only the translation pointer field to determine the mode
|
|
$result = $queryBuilder
|
|
->select($transOrigPointerField)
|
|
->from('tt_content')
|
|
->where(
|
|
$queryBuilder->expr()->eq(
|
|
'pid',
|
|
$queryBuilder->createNamedParameter($pageId, Connection::PARAM_INT)
|
|
),
|
|
$queryBuilder->expr()->eq(
|
|
$languageField,
|
|
$queryBuilder->createNamedParameter($targetLanguage, Connection::PARAM_INT)
|
|
)
|
|
)
|
|
->executeQuery();
|
|
|
|
$hasRecords = false;
|
|
$hasConnected = false;
|
|
|
|
// Iterate through results to detect the mode
|
|
while ($row = $result->fetchAssociative()) {
|
|
$hasRecords = true;
|
|
$transOrigPointer = (int)($row[$transOrigPointerField] ?? 0);
|
|
|
|
// If we find any connected record (transOrigPointer > 0), return TRANSLATE immediately
|
|
// This prevents mixing modes even if there are also free mode records
|
|
if ($transOrigPointer > 0) {
|
|
$hasConnected = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// No records found
|
|
if (!$hasRecords) {
|
|
return null;
|
|
}
|
|
|
|
// If any connected record exists, return TRANSLATE mode
|
|
// Otherwise, all records are free mode, return COPY
|
|
return $hasConnected ? LocalizationMode::TRANSLATE : LocalizationMode::COPY;
|
|
}
|
|
|
|
private function getBackendUser(): BackendUserAuthentication
|
|
{
|
|
return $GLOBALS['BE_USER'];
|
|
}
|
|
|
|
private function getLanguageService(): LanguageService
|
|
{
|
|
return $GLOBALS['LANG'];
|
|
}
|
|
}
|