diff --git a/typo3/sysext/core/Classes/Domain/Repository/PageRepository.php b/typo3/sysext/core/Classes/Domain/Repository/PageRepository.php index b5c2020cc44171b4952c6b4ebdd6961cfabf43c4..b9aa25ea2072ef81f90bde8ccae931a667d022d7 100644 --- a/typo3/sysext/core/Classes/Domain/Repository/PageRepository.php +++ b/typo3/sysext/core/Classes/Domain/Repository/PageRepository.php @@ -365,8 +365,7 @@ class PageRepository implements LoggerAwareInterface /** * Master helper method to overlay a record to a language. * - * Be aware that for pages the languageId is taken, and for all other records the contentId. - * This might change through a feature switch in the future. + * Be aware that for pages the languageId is taken, and for all other records the contentId of the Aspect is used. * * @param string $table the name of the table, should be a TCA table with localization enabled * @param array $originalRow the current (full-fletched) record. @@ -398,10 +397,57 @@ class PageRepository implements LoggerAwareInterface $localizedRecord = null; if ($languageAspect->doOverlays()) { $attempted = true; - if ($table === 'pages') { - $localizedRecord = $this->getPageOverlay($originalRow, $languageAspect); + // Mixed = if nothing is available in the selected language, try the fallbacks + // Fallbacks work as follows: + // 1. We have a default language record and then start doing overlays (= the basis for fallbacks) + // 2. Check if the actual requested language version is available in the DB (language=3 = canadian-french) + // 3. If not, we check the next language version in the chain (e.g. language=2 = french) and so forth until we find a record + if ($languageAspect->getOverlayType() === LanguageAspect::OVERLAYS_MIXED) { + $languageChain = $this->getLanguageFallbackChain($languageAspect); + $languageChain = array_reverse($languageChain); + if ($table === 'pages') { + $result = $this->getPageOverlay( + $originalRow, + new LanguageAspect($languageAspect->getId(), $languageAspect->getId(), LanguageAspect::OVERLAYS_MIXED, $languageChain) + ); + if (!empty($result)) { + $localizedRecord = $result; + } + } else { + $languageChain = array_merge($languageChain, [$languageAspect->getContentId()]); + // Loop through each (fallback) language and see if there is a record + // However, we do not want to preserve the "originalRow", that's why we set the option to "OVERLAYS_ON" + while (($languageId = array_pop($languageChain)) !== null) { + $result = $this->getRecordOverlay( + $table, + $originalRow, + new LanguageAspect($languageId, $languageId, LanguageAspect::OVERLAYS_ON) + ); + // If an overlay is found, return it + if (is_array($result)) { + $localizedRecord = $result; + break; + } + } + if ($localizedRecord === null) { + // If nothing was found, we set the localized record to the originalRow to simulate + // that the default language is "kept" (we want fallback to default language). + // Note: Most installations might have "type=fallback" set but do not set the default language + // as fallback. In the future - once we want to get rid of the magic "default language", + // this needs to behave different, and the "pageNotFound" special handling within fallbacks should be removed + // and we need to check explicitly on in_array(0, $languageAspect->getFallbackChain()) + // However, getPageOverlay() a few lines above also returns the "default language page" as well. + $localizedRecord = $originalRow; + } + } } else { - $localizedRecord = $this->getRecordOverlay($table, $originalRow, $languageAspect); + // The option to hide records if they were not explicitly selected, was chosen (OVERLAYS_ON/WITH_FLOATING) + // in the language configuration. So, here no changes are done. + if ($table === 'pages') { + $localizedRecord = $this->getPageOverlay($originalRow, $languageAspect); + } else { + $localizedRecord = $this->getRecordOverlay($table, $originalRow, $languageAspect); + } } } @@ -740,7 +786,7 @@ class PageRepository implements LoggerAwareInterface } } } - return $row; + return is_array($row) ? $row : null; } /************************************************ diff --git a/typo3/sysext/core/Documentation/Changelog/12.2/Feature-88137-Multi-levelFallbackForContentInFrontendRendering.rst b/typo3/sysext/core/Documentation/Changelog/12.2/Feature-88137-Multi-levelFallbackForContentInFrontendRendering.rst new file mode 100644 index 0000000000000000000000000000000000000000..d9018230c14f0242715466fab38c1f5281c7ed0e --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/12.2/Feature-88137-Multi-levelFallbackForContentInFrontendRendering.rst @@ -0,0 +1,58 @@ +.. include:: /Includes.rst.txt + +.. _feature-88137-1673993076: + +======================================================================== +Feature: #88137 - Multi-level fallback for content in frontend rendering +======================================================================== + +See :issue:`88137` + +Description +=========== + +TYPO3's Site Handling was introduced in TYPO3 v9 and allows to define a +"Fallback Type". + +A fallback type allows to define the behavior of how pages and the content +should be fetched from the database when rendering a page in the frontend. + +The option "strict" only renders content which was explicitly translated or +created in this defined language, and keeps the sorting behavior of the +default language. + +The option "free" does not consider the default language or its sorting, +and only directly fetches content of the given Language ID. + +The option "fallback" allows to define a fallback chain of other languages. +When a certain page in the given language is not available or created, TYPO3 +first checks the fallback chain if a page is available in one of the languages +of the fallback chain. + +A common scenario is this: +* German (Austria) - Language = 2 +* German (Germany) - Language = 1 +* English (Default) - Language = 0 + +TYPO3 now can deal with the language chain in fallback mode not only for pages, +but also for any kind of content. + + +Impact +====== + +When working in a scenario with "fallback" and multiple languages in the fallback +chain, TYPO3 now checks for each content if the target language is available, +and then checks for the same content if it is translated in the language of the +fallback chain (example above in "German (Germany)"), before falling back to +the default language - which was the behavior until now. + +The language chain processing works with fallback mode (a.k.a. "overlays in mixed mode"), +both in TypoScript and Extbase code. Under the hood, the method +:php:`PageRepository->getLanguageOverlay()` is responsible for the chaining. + +Current limitations: +* Content fallback only works in fallbackType=fallback +* Content fallback always stops at the default language (as this was the previous behavior) + +.. index:: Frontend, PHP-API, TypoScript, ext:core diff --git a/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Storage/Typo3DbBackendTest.php b/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Storage/Typo3DbBackendTest.php index 6f6780646e81c727a8e55112c293e7689e284d2d..26eee5252e8de41973818d0826f53aa8627e9142 100644 --- a/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Storage/Typo3DbBackendTest.php +++ b/typo3/sysext/extbase/Tests/Functional/Persistence/Generic/Storage/Typo3DbBackendTest.php @@ -21,6 +21,7 @@ use ExtbaseTeam\BlogExample\Domain\Model\Tag; use ExtbaseTeam\BlogExample\Domain\Repository\BlogRepository; use ExtbaseTeam\BlogExample\Domain\Repository\PostRepository; use TYPO3\CMS\Core\Context\Context; +use TYPO3\CMS\Core\Context\LanguageAspect; use TYPO3\CMS\Core\Context\WorkspaceAspect; use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder; use TYPO3\CMS\Core\Http\ServerRequest; @@ -71,6 +72,7 @@ class Typo3DbBackendTest extends FunctionalTestCase $blogRepository = $this->get(BlogRepository::class); $context = new Context([ 'workspace' => new WorkspaceAspect(1), + 'language' => new LanguageAspect(0, 0), ]); GeneralUtility::setSingletonInstance(Context::class, $context); $querySettings = new Typo3QuerySettings($context, $this->get(ConfigurationManagerInterface::class)); diff --git a/typo3/sysext/frontend/Tests/Functional/Rendering/LocalizedSiteContentRenderingTest.php b/typo3/sysext/frontend/Tests/Functional/Rendering/LocalizedSiteContentRenderingTest.php index feaa9066354b5f5462e714641015558e70598ab2..16fd8a0e789ddbf177efc981ea8bd8d356300d5a 100644 --- a/typo3/sysext/frontend/Tests/Functional/Rendering/LocalizedSiteContentRenderingTest.php +++ b/typo3/sysext/frontend/Tests/Functional/Rendering/LocalizedSiteContentRenderingTest.php @@ -87,7 +87,7 @@ use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\ResponseContent; * * LanguageAspect * -> doOverlays() - * whether the the overlay logic should be applied + * whether the overlay logic should be applied * -> getLanguageId() * the language that was originally requested * -> getContentId() @@ -681,113 +681,110 @@ class LocalizedSiteContentRenderingTest extends AbstractDataHandlerActionTestCas self::assertEquals($statusCode, $response->getStatusCode()); } - public function contentOnPartiallyTranslatedPageDataProvider(): array + public function contentOnPartiallyTranslatedPageDataProvider(): \Generator { //Expected behaviour: //Setting sys_language_mode to different values doesn't influence the result as the requested page is translated to Polish, //Page title is always [PL]Page, and both languageId/contentId are always 3 - return [ - [ - 'languageConfiguration' => [ - 'fallbackType' => 'free', - ], - 'visibleRecordHeaders' => ['[Translate to Polski:] Regular Element #1', '[PL] Without default language'], + yield 'free' => [ + 'languageConfiguration' => [ 'fallbackType' => 'free', - 'fallbackChain' => 'pageNotFound', - 'overlayMode' => 'off', ], - [ - 'languageConfiguration' => [ - 'fallbackType' => 'free', - 'fallbackChain' => ['EN'], - ], - 'visibleRecordHeaders' => ['[Translate to Polski:] Regular Element #1', '[PL] Without default language'], + 'visibleRecordHeaders' => ['[Translate to Polski:] Regular Element #1', '[PL] Without default language'], + 'fallbackType' => 'free', + 'fallbackChain' => 'pageNotFound', + 'overlayMode' => 'off', + ]; + yield 'free with fallback' => [ + 'languageConfiguration' => [ 'fallbackType' => 'free', - 'fallbackChain' => '0,pageNotFound', - 'overlayMode' => 'off', + 'fallbackChain' => ['EN'], ], - [ - 'languageConfiguration' => [ - 'fallbackType' => 'free', - 'fallbackChain' => ['DK', 'EN'], - ], - 'visibleRecordHeaders' => ['[Translate to Polski:] Regular Element #1', '[PL] Without default language'], + 'visibleRecordHeaders' => ['[Translate to Polski:] Regular Element #1', '[PL] Without default language'], + 'fallbackType' => 'free', + 'fallbackChain' => '0,pageNotFound', + 'overlayMode' => 'off', + ]; + yield 'free with multiple fallbacks' => [ + 'languageConfiguration' => [ 'fallbackType' => 'free', - 'fallbackChain' => '1,0,pageNotFound', - 'overlayMode' => 'off', + 'fallbackChain' => ['DK', 'EN'], ], - // Expected behaviour: - // Not translated element #2 is shown because sys_language_overlay = 1 (with sys_language_overlay = hideNonTranslated, it would be hidden) - [ - 'languageConfiguration' => [ - 'fallbackType' => 'fallback', - 'fallbackChain' => ['EN'], - ], - 'visibleRecordHeaders' => ['[Translate to Polski:] Regular Element #1', 'Regular Element #2', 'Regular Element #3'], + 'visibleRecordHeaders' => ['[Translate to Polski:] Regular Element #1', '[PL] Without default language'], + 'fallbackType' => 'free', + 'fallbackChain' => '1,0,pageNotFound', + 'overlayMode' => 'off', + ]; + // Expected behaviour: + // Not translated element #2 is shown because sys_language_overlay = 1 (with sys_language_overlay = hideNonTranslated, it would be hidden) + yield 'fallback to EN' => [ + 'languageConfiguration' => [ 'fallbackType' => 'fallback', - 'fallbackChain' => '0,pageNotFound', - 'overlayMode' => 'mixed', + 'fallbackChain' => ['EN'], ], - // Expected behaviour: - // Element #3 is not translated in PL and it is translated in DK. It's not shown as content_fallback is not related to single CE level - // but on page level - and this page is translated to Polish, so no fallback is happening - [ - 'languageConfiguration' => [ - 'fallbackType' => 'fallback', - 'fallbackChain' => ['DK', 'EN'], - ], - 'visibleRecordHeaders' => ['[Translate to Polski:] Regular Element #1', 'Regular Element #2', 'Regular Element #3'], + 'visibleRecordHeaders' => ['[Translate to Polski:] Regular Element #1', 'Regular Element #2', 'Regular Element #3'], + 'fallbackType' => 'fallback', + 'fallbackChain' => '0,pageNotFound', + 'overlayMode' => 'mixed', + ]; + // Expected behaviour: + // Element #3 is not translated in PL, but it is translated in DK. The DK version is shown as it has a fallback chain defined. + yield 'fallback with multiple languages' => [ + 'languageConfiguration' => [ 'fallbackType' => 'fallback', - 'fallbackChain' => '1,0,pageNotFound', - 'overlayMode' => 'mixed', + 'fallbackChain' => ['DK', 'EN'], ], - [ - 'languageConfiguration' => [ - 'fallbackType' => 'fallback', - 'fallbackChain' => [], - ], - 'visibleRecordHeaders' => ['[Translate to Polski:] Regular Element #1', 'Regular Element #2', 'Regular Element #3'], + 'visibleRecordHeaders' => ['[Translate to Polski:] Regular Element #1', 'Regular Element #2', '[Translate to Dansk:] Regular Element #3'], + 'fallbackType' => 'fallback', + 'fallbackChain' => '1,0,pageNotFound', + 'overlayMode' => 'mixed', + ]; + yield 'fallback but without languages' => [ + 'languageConfiguration' => [ 'fallbackType' => 'fallback', - 'fallbackChain' => 'pageNotFound', - 'overlayMode' => 'mixed', + 'fallbackChain' => [], ], - // Expected behaviour: - // Non translated default language elements are not shown, because of hideNonTranslated - [ - 'languageConfiguration' => [ - 'fallbackType' => 'strict', - 'fallbackChain' => ['EN'], - ], - 'visibleRecordHeaders' => ['[Translate to Polski:] Regular Element #1', '[PL] Without default language'], + 'visibleRecordHeaders' => ['[Translate to Polski:] Regular Element #1', 'Regular Element #2', 'Regular Element #3'], + 'fallbackType' => 'fallback', + 'fallbackChain' => 'pageNotFound', + 'overlayMode' => 'mixed', + ]; + // Expected behaviour: + // Non translated default language elements are not shown, because of hideNonTranslated + yield 'strict with fallback to default' => [ + 'languageConfiguration' => [ 'fallbackType' => 'strict', - 'fallbackChain' => '0,pageNotFound', - 'overlayMode' => 'includeFloating', + 'fallbackChain' => ['EN'], ], - [ - 'languageConfiguration' => [ - 'fallbackType' => 'strict', - 'fallbackChain' => ['DK', 'EN'], - ], - 'visibleRecordHeaders' => ['[Translate to Polski:] Regular Element #1', '[PL] Without default language'], + 'visibleRecordHeaders' => ['[Translate to Polski:] Regular Element #1', '[PL] Without default language'], + 'fallbackType' => 'strict', + 'fallbackChain' => '0,pageNotFound', + 'overlayMode' => 'includeFloating', + ]; + yield 'strict with multiple fallbacks' => [ + 'languageConfiguration' => [ 'fallbackType' => 'strict', - 'fallbackChain' => '1,0,pageNotFound', - 'overlayMode' => 'includeFloating', + 'fallbackChain' => ['DK', 'EN'], ], - [ - 'languageConfiguration' => [ - 'fallbackType' => 'strict', - 'fallbackChain' => [], - ], - 'visibleRecordHeaders' => ['[Translate to Polski:] Regular Element #1', '[PL] Without default language'], + 'visibleRecordHeaders' => ['[Translate to Polski:] Regular Element #1', '[PL] Without default language'], + 'fallbackType' => 'strict', + 'fallbackChain' => '1,0,pageNotFound', + 'overlayMode' => 'includeFloating', + ]; + yield 'strict without fallback' => [ + 'languageConfiguration' => [ 'fallbackType' => 'strict', - 'fallbackChain' => 'pageNotFound', - 'overlayMode' => 'includeFloating', + 'fallbackChain' => [], ], + 'visibleRecordHeaders' => ['[Translate to Polski:] Regular Element #1', '[PL] Without default language'], + 'fallbackType' => 'strict', + 'fallbackChain' => 'pageNotFound', + 'overlayMode' => 'includeFloating', ]; } /** - * Page uid 89 is translated to to Polish, but not all CE are translated + * Page uid 89 is translated to Polish, but not all CE are translated * * @test * @dataProvider contentOnPartiallyTranslatedPageDataProvider