[FEATURE] Re-introduce mixed overlay mode for content fallback 67/60367/6
authorBenni Mack <benni@typo3.org>
Wed, 3 Apr 2019 14:29:42 +0000 (16:29 +0200)
committerAnja Leichsenring <aleichsenring@ab-softlab.de>
Fri, 12 Apr 2019 17:50:51 +0000 (19:50 +0200)
This "feature" solves a lot of issues, but I really wanted to make sure
to cover the generic issues around this topic.

In order to understand this, we need to understand the different
logic when talking about "language fallbacks", but I don't want
to warm up how it was before.

This is how it works:
- fallbackChain: defines which page translation should be checked
when linking or resolving a page in a specific language
- fallbackType: Now that we covered all relevant cases, the naming
could be better, but here we go, it's actually defining the logic for fetching
content.
- fallbackType="strict" -> only show the content that is viable in the target language
  but this is based on "overlays". Fetch all "language=0" records, do overlays
  and remove the ones that have no overlays. However, take the ones that have no
  language parent and render them as well ("includeWithoutDefaultTranlsation")
  This is what we call "do overlays with floating".
  This is recommended to do in most classic translation cases, with different languages
- fallbackType="fallback" -> Do overlays: Fetch all "language=0" records, do overlays
  but KEEP the ones that have no overlays. However, take the ones that have no
  language parent and render them as well ("includeWithoutDefaultTranlsation")
  This is what we call "do overlays in mixed mode".
  Useful if your translation is Swiss-German but your default language is "German"
- fallbackType="free" - Do not do overlays, just fetch all records of the target language
  Could be seen as "free mode" as we do it in TYPO3 Page module.

The new free option is therefore new, also the "fallback" functionality
is actually showing more content than before (thus, different, but maybe we could fix that!)
as we have the "mixed" mode back.
Also the "language fallback" is now possible for any fallbackType.

Now, what's still missing - but out of scope - is actually a way to fetch content
with multiple possibilities for overlaying. I call this "forward language overlays"
however, this is a feature that is theoretically possible but not in v9 anymore.

This patch restores the max. types of use cases back for TYPO3 Core.

The following things that are really gone for good now and won't come back:
- config.sys_language_mode = ignore
- config.sys_language_mode =
- Option includeRecordsWithoutDefaultTranslation (= always enabled) is not needed anymore

Also, there are no ways anymore to use inconsistent multiple TypoScript settings which
do not make sense depending if the translated page does not exist (l18n_cfg)
but still using TypoScript conditions for that.

Resolves: #86762
Resolves: #86712
Releases: master, 9.5
Change-Id: I8b3144410f7d7ed1d705d42f16a46f190275387a
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/60367
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Nikolaj Wojtkowiak-Pfänder <nwp@dr-bock.com>
Tested-by: Steven Hilgendorff
Tested-by: Ralf Merz <mail@merzilla.de>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Nikolaj Wojtkowiak-Pfänder <nwp@dr-bock.com>
Reviewed-by: Ralf Merz <mail@merzilla.de>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
typo3/sysext/backend/Configuration/SiteConfiguration/site_language.php
typo3/sysext/core/Classes/Context/LanguageAspectFactory.php
typo3/sysext/core/Classes/Routing/PageRouter.php
typo3/sysext/core/Documentation/Changelog/9.5.x/Feature-86762-EnhancedFallbackModesForTranslatedContent.rst [new file with mode: 0644]
typo3/sysext/frontend/Tests/Functional/Rendering/LocalizedSiteContentRenderingTest.php [new file with mode: 0644]

index fcee054..4ec2434 100644 (file)
@@ -405,14 +405,15 @@ return [
                 'type' => 'select',
                 'renderType' => 'selectSingle',
                 'items' => [
-                    ['No fallback (strict)', 'strict'],
-                    ['Fallback to other language', 'fallback'],
+                    ['Strict: Show only translated content, based on overlays', 'strict'],
+                    ['Fallback: Show default language if no translation exists', 'fallback'],
+                    ['Free mode: Ignore translation and overlay concept, only show data from selected language', 'free'],
                 ],
             ],
         ],
         'fallbacks' => [
             'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:site_language.fallbacks',
-            'displayCond' => 'FIELD:fallbackType:=:fallback',
+            'displayCond' => 'FIELD:languageId:>:0',
             'config' => [
                 'type' => 'select',
                 'renderType' => 'selectMultipleSideBySide',
index 0d6c5a9..5d231ea 100644 (file)
@@ -103,15 +103,32 @@ class LanguageAspectFactory
     {
         $languageId = $language->getLanguageId();
         $fallbackType = $language->getFallbackType();
-        if ($fallbackType === 'fallback') {
-            $fallbackOrder = $language->getFallbackLanguageIds();
-            $fallbackOrder[] = 'pageNotFound';
-        } elseif ($fallbackType === 'strict') {
-            $fallbackOrder = [];
-        } else {
-            $fallbackOrder = [0];
+        $fallbackOrder = $language->getFallbackLanguageIds();
+        $fallbackOrder[] = 'pageNotFound';
+        switch ($fallbackType) {
+            // Fall back to other language, if the page does not exist in the requested language
+            // But always fetch only records of this specific (available) language
+            case 'free':
+                $overlayType = LanguageAspect::OVERLAYS_OFF;
+                break;
+
+            // Fall back to other language, if the page does not exist in the requested language
+            // Do overlays, and keep the ones that are not translated
+            case 'fallback':
+                $overlayType = LanguageAspect::OVERLAYS_MIXED;
+                break;
+
+            // Same as "fallback" but remove the records that are not translated
+            case 'strict':
+                $overlayType = LanguageAspect::OVERLAYS_ON_WITH_FLOATING;
+                break;
+
+            // Ignore, fallback to default language
+            default:
+                $fallbackOrder = [0];
+                $overlayType = LanguageAspect::OVERLAYS_OFF;
         }
 
-        return GeneralUtility::makeInstance(LanguageAspect::class, $languageId, $languageId, LanguageAspect::OVERLAYS_ON_WITH_FLOATING, $fallbackOrder);
+        return GeneralUtility::makeInstance(LanguageAspect::class, $languageId, $languageId, $overlayType, $fallbackOrder);
     }
 }
index 00d04d5..ac477b0 100644 (file)
@@ -119,7 +119,7 @@ class PageRouter implements RouterInterface
         $pageCandidates = [];
         $language = $previousResult->getLanguage();
         $languages = [$language->getLanguageId()];
-        if ($language->getFallbackType() === 'fallback') {
+        if (!empty($language->getFallbackLanguageIds())) {
             $languages = array_merge($languages, $language->getFallbackLanguageIds());
         }
         // Iterate all defined languages in their configured order to get matching page candidates somewhere in the language fallback chain
diff --git a/typo3/sysext/core/Documentation/Changelog/9.5.x/Feature-86762-EnhancedFallbackModesForTranslatedContent.rst b/typo3/sysext/core/Documentation/Changelog/9.5.x/Feature-86762-EnhancedFallbackModesForTranslatedContent.rst
new file mode 100644 (file)
index 0000000..152b8ba
--- /dev/null
@@ -0,0 +1,66 @@
+.. include:: ../../Includes.txt
+
+================================================================
+Feature: #86762 - Enhanced fallback modes for translated content
+================================================================
+
+See :issue:`86762`
+
+Description
+===========
+
+Various content fallback options have been adapted to allow multiple scenarios when rendering
+content in a different language than the default language (sys_language_uid=0).
+
+The functionality of "fallbackChain" can now be defined in any kind of fallback type (see below).
+
+The "fallbackChain" checks access / availability of a page translation of a language. And if
+this language does not exist, TYPO3 checks for other languages, and uses this language then
+for showing content.
+
+This results in three different kinds of rendering modes ("Fallback Type") for content in
+translated content, however it is necessary to understand the overlay concept when fetching
+content in TYPO3 Frontend.
+
+Using "language overlays" means that the default language records are fetched at first.
+Also, various "enable fields" (e.g. hidden / frontend user groups etc) are evaluated for the
+default language. Each record then is "overlaid" with the record of the target language.
+
+Not using "overlays" means that the default language is not considered at all.
+
+No matter what type is chosen, records which do not have a localization parent ("l10n_parent")
+will always be rendered in the target language.
+
+The following "fallback types" exist:
+
+1. "strict" -- Fetch the records in the default language, then overlay them with the target
+language. If a record is not translated into the target language, then it is not shown at all.
+
+This mode is typically used for 1:1 translations of fully different languages like
+"English" (default) and "Danish" (translation).
+
+2. "fallback" -- Fetch records from default language, and checks for a translation of
+each record. If the record has no translation, the default language is still shown.
+
+This scenario is usually used when the default language is "German" but the translation
+is "Swiss-German" where only different content elements are translated, but the rest is
+a 1:1 translation.
+
+3. "free" (new) -- Fetch all records from the target language directly without worrying about
+the default language at all.
+
+This is typically the case when a localized page may have fully different content than the
+default language. E.g. "English" as default language, but only the most important content parts
+are added in language "Swahili".
+
+
+Impact
+======
+
+Existing installations with site configuration "fallback" will also now render the non-translated
+content (un-localized records).
+
+Regardless of the fallback type, records without localization parent, and records set to "-1"
+(All Languages) are always fetched.
+
+.. index:: Frontend
diff --git a/typo3/sysext/frontend/Tests/Functional/Rendering/LocalizedSiteContentRenderingTest.php b/typo3/sysext/frontend/Tests/Functional/Rendering/LocalizedSiteContentRenderingTest.php
new file mode 100644 (file)
index 0000000..61f43c3
--- /dev/null
@@ -0,0 +1,937 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Frontend\Tests\Functional\Rendering;
+
+/*
+ * 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!
+ */
+
+use TYPO3\CMS\Core\Error\Http\PageNotFoundException;
+use TYPO3\CMS\Core\Tests\Functional\SiteHandling\SiteBasedTestTrait;
+use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
+use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\ResponseContent;
+
+/**
+ * Test case checking if localized tt_content is rendered correctly with different language settings
+ * with site configuration.
+ *
+ * Previously the language was given by TypoScript settings which were overriden via GP parameters for language
+ *
+ * config.sys_language_uid = [0,1,2,3,4...]
+ * config.sys_language_mode = [strict, content_fallback;2,3, ignore, '']
+ * config.sys_language_overlay = [0, 1, hideNonTranslated]
+ *
+ * The previous setting config.sys_language_mode the behaviour of the page translation was referred to, and what
+ * should happen if a page translation does not exist.
+ *
+ * the setting config.sys_language_overlay was responsible if records of a target language should be fetched
+ * directly ("free mode" or no-overlays), or if the default language (L=0) should be taken and then overlaid.
+ * In addition the "hideNonTranslated" was a special form of overlays: Take the default language, but if the translation
+ * does not exist, do not render the default language.
+ *
+ * This is what changed with Site Handling:
+ * - General approach is now defined on a site language, in configuration, not evaluated during runtime
+ * - Page fallback concept (also for menu generation) is now valid for page and content.
+ * - Various options which only made sense on specific page configurations have been removed for consistency reasons.
+ *
+ * Pages & Menus:
+ * - When a Page Translation needs to be fetched, it is checked if the page translation exists, otherwise the "fallbackChain"
+ * jumps in and checks if the other languages are available or aren't available.
+ * - If no fallbackChain is given, then the page is not shown / rendered / accessible.
+ * - pages.l18n_cfg is now considered properly with multiple fallback languages for menus and page resolving and URL linking.
+ *
+ * Content Fetching:
+ *
+ * - A new "free" mode only fetches the records that are set in a specific language.
+ *   Due to the concept of the database structure, no fallback logic applies currently when selecting records, however
+ *   fallbackChains are still valid for identifying the Page Translation.
+ * - The modes "fallback" and "strict" have similarities: They utilize the so-called "overlay" logic: Fetch records in the default
+ *   language (= 0) and then overlay with the available language. This ensures that ordering and other connections
+ *   are kept the same way as on the default language.
+ * - "fallback" shows content in the language of the page that was selected, does the overlays but keeps the default
+ *   language records when no translation is available (= "mixed overlays").
+ * - "strict" shows only content of the page that was selected via overlays (fetch default language and do overlays)
+ *    but does not render the ones that have no translation in the specific language.
+ *
+ * General notes regarding content fetching:
+ * - Records marked as "All Languages" (sys_language_uid = -1) are always fetched (this wasn't always the case before!).
+ * - Records without a language parent (l10n_parent) are rendered at any time.
+ *
+ * Relevant parts for site handling:
+ *
+ * SiteLanguage
+ * -> languageId
+ *    the language that is requested, usually determined by the base property. If this setting is "0"
+ *    no other options are taken into account.
+ * -> fallbackType
+ *    - strict:
+ *        * for pages: if the page translation does not exist, check fallbackChain
+ *        * for record fetching: take Default Language records which have a valid translation for this language + records without default translation
+ *    - fallback:
+ *        * for pages: if the page translation does not exist, check fallbackChain
+ *        * for record fetching: take Default Language records and overlay the language, but keep default language records + records without default translation
+ *    - free:
+ *        * for pages: if the page translation does not exist, check fallbackChain
+ *        * for record fetching: Only fetch records of the current language and "All languages" no overlays are done.
+ *
+ * LanguageAspect
+ * -> doOverlays()
+ *    whether the the overlay logic should be applied
+ * -> getLanguageId()
+ *    the language that was originally requested
+ * -> getContentId()
+ *    if the page translation for e.g. language=5 is not available, but the fallback is "4,3,2", then the content of this language is used instead.
+ *    applies to all concepts of fallback types.
+ * -> getFallbackChain()
+ *    if the page is not available in a specific language, apply other language Ids in the given order until the page translation can be found.
+ */
+class LocalizedSiteContentRenderingTest extends \TYPO3\CMS\Core\Tests\Functional\DataHandling\AbstractDataHandlerActionTestCase
+{
+    use SiteBasedTestTrait;
+
+    const VALUE_PageId = 89;
+    const TABLE_Content = 'tt_content';
+    const TABLE_Pages = 'pages';
+
+    /**
+     * @var string
+     */
+    protected $scenarioDataSetDirectory = 'typo3/sysext/frontend/Tests/Functional/Rendering/DataSet/';
+
+    /**
+     * @var string[]
+     */
+    protected $coreExtensionsToLoad = ['frontend', 'workspaces'];
+
+    /**
+     * @var array
+     */
+    protected $pathsToLinkInTestInstance = [
+        'typo3/sysext/core/Tests/Functional/Fixtures/Frontend/AdditionalConfiguration.php' => 'typo3conf/AdditionalConfiguration.php',
+        'typo3/sysext/frontend/Tests/Functional/Fixtures/Images' => 'fileadmin/user_upload'
+    ];
+
+    /**
+     * If this value is NULL, log entries are not considered.
+     * If it's an integer value, the number of log entries is asserted.
+     *
+     * @var int|null
+     */
+    protected $expectedErrorLogEntries = null;
+
+    /**
+     * @var array
+     */
+    protected const LANGUAGE_PRESETS = [
+        'EN' => ['id' => 0, 'title' => 'English', 'locale' => 'en_US.UTF8'],
+        'DK' => ['id' => 1, 'title' => 'Dansk', 'locale' => 'dk_DA.UTF8'],
+        'DE' => ['id' => 2, 'title' => 'Deutsch', 'locale' => 'de_DE.UTF8'],
+        'PL' => ['id' => 3, 'title' => 'Polski', 'locale' => 'pl_PL.UTF8'],
+    ];
+
+    protected function setUp()
+    {
+        parent::setUp();
+
+        $this->importDataSet('PACKAGE:typo3/testing-framework/Resources/Core/Functional/Fixtures/sys_file_storage.xml');
+        $this->importScenarioDataSet('LiveDefaultPages');
+        $this->importScenarioDataSet('LiveDefaultElements');
+
+        $this->setUpFrontendRootPage(1, [
+            'typo3/sysext/core/Tests/Functional/Fixtures/Frontend/JsonRenderer.typoscript',
+        ]);
+    }
+
+    /**
+     * For the default language all combination of language settings should give the same result,
+     * regardless of Language fallback settings, if the default language is requested then no language settings apply.
+     *
+     * @test
+     */
+    public function onlyEnglishContentIsRenderedForDefaultLanguage()
+    {
+        $this->writeSiteConfiguration(
+            'test',
+            $this->buildSiteConfiguration(1, 'https://website.local/'),
+            [
+                $this->buildDefaultLanguageConfiguration('EN', '/en/'),
+                $this->buildLanguageConfiguration('DK', '/dk/')
+            ],
+            [
+                $this->buildErrorHandlingConfiguration('Fluid', [404])
+            ]
+        );
+
+        $response = $this->executeFrontendRequest(
+            new InternalRequest('https://website.local/en/?id=' . static::VALUE_PageId)
+        );
+        $responseStructure = ResponseContent::fromString((string)$response->getBody());
+
+        $responseSections = $responseStructure->getSections();
+        $visibleHeaders = ['Regular Element #1', 'Regular Element #2', 'Regular Element #3'];
+        $this->assertThat(
+            $responseSections,
+            $this->getRequestSectionHasRecordConstraint()
+            ->setTable(self::TABLE_Content)
+            ->setField('header')
+            ->setValues(...$visibleHeaders)
+        );
+        $this->assertThat(
+            $responseSections,
+            $this->getRequestSectionDoesNotHaveRecordConstraint()
+            ->setTable(self::TABLE_Content)
+            ->setField('header')
+            ->setValues(...$this->getNonVisibleHeaders($visibleHeaders))
+        );
+
+        //assert FAL relations
+        $visibleFiles = ['T3BOARD'];
+        $this->assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
+            ->setRecordIdentifier(self::TABLE_Content . ':297')->setRecordField('image')
+            ->setTable('sys_file_reference')->setField('title')->setValues(...$visibleFiles));
+
+        $this->assertThat($responseSections, $this->getRequestSectionStructureDoesNotHaveRecordConstraint()
+            ->setRecordIdentifier(self::TABLE_Content . ':297')->setRecordField('image')
+            ->setTable('sys_file_reference')->setField('title')->setValues(...$this->getNonVisibleFileTitles($visibleFiles)));
+
+        $visibleFiles = ['Kasper2'];
+        $this->assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
+            ->setRecordIdentifier(self::TABLE_Content . ':298')->setRecordField('image')
+            ->setTable('sys_file_reference')->setField('title')->setValues(...$visibleFiles));
+
+        $this->assertThat($responseSections, $this->getRequestSectionStructureDoesNotHaveRecordConstraint()
+            ->setRecordIdentifier(self::TABLE_Content . ':298')->setRecordField('image')
+            ->setTable('sys_file_reference')->setField('title')->setValues(...$this->getNonVisibleFileTitles($visibleFiles)));
+
+        // Assert language settings and page record title
+        $this->assertEquals('Default language Page', $responseStructure->getScopePath('page/title'));
+        $this->assertEquals(0, $responseStructure->getScopePath('languageInfo/id'), 'languageId does not match');
+        $this->assertEquals(0, $responseStructure->getScopePath('languageInfo/contentId'), 'contentId does not match');
+        $this->assertEquals('strict', $responseStructure->getScopePath('languageInfo/fallbackType'), 'fallbackType does not match');
+        $this->assertEquals('pageNotFound', $responseStructure->getScopePath('languageInfo/fallbackChain'), 'fallbackChain does not match');
+        $this->assertEquals('includeFloating', $responseStructure->getScopePath('languageInfo/overlayType'), 'language overlayType does not match');
+    }
+
+    /**
+     * Dutch language has page translation record and some content elements are translated
+     *
+     * @return array
+     */
+    public function dutchDataProvider(): array
+    {
+        return [
+            [
+                // Only records with language=1 are shown
+                'languageConfiguration' => [
+                    'fallbackType' => 'free'
+                ],
+                'visibleRecords' => [
+                    300 => [
+                        'header' => '[Translate to Dansk:] Regular Element #3',
+                        'image' => ['[Kasper] Image translated to Dansk', '[T3BOARD] Image added in Dansk (without parent)'],
+                    ],
+                    301 => [
+                        'header' => '[Translate to Dansk:] Regular Element #1',
+                        'image' => [],
+                    ],
+                    303 => [
+                        'header' => '[DK] Without default language',
+                        'image' => ['[T3BOARD] Image added to DK element without default language']
+                    ],
+                    308 => [
+                        'header' => '[DK] UnHidden Element #4',
+                        'image' => []
+                    ],
+                ],
+                'fallbackType' => 'free',
+                'fallbackChain' => 'pageNotFound',
+                'overlayMode' => 'off',
+            ],
+            // Expected behaviour:
+            // Not translated element #2 is shown because "fallback" is enabled, which defaults to L=0 elements
+            [
+                'languageConfiguration' => [
+                    'fallbackType' => 'fallback'
+                ],
+                'visibleRecords' => [
+                    297 => [
+                        'header' => '[Translate to Dansk:] Regular Element #1',
+                        'image' => [],
+                    ],
+                    298 => [
+                        'header' => 'Regular Element #2',
+                        'image' => ['Kasper2'],
+                    ],
+                    299 => [
+                        'header' => '[Translate to Dansk:] Regular Element #3',
+                        'image' => ['[Kasper] Image translated to Dansk', '[T3BOARD] Image added in Dansk (without parent)'],
+                    ],
+                ],
+                'fallbackType' => 'fallback',
+                'fallbackChain' => 'pageNotFound',
+                'overlayMode' => 'mixed',
+            ],
+            // Expected behaviour:
+            // Non translated default language elements are not shown, but the results include the records without default language as well
+            [
+                'languageConfiguration' => [
+                    'fallbackType' => 'strict'
+                ],
+                'visibleRecords' => [
+                    297 => [
+                        'header' => '[Translate to Dansk:] Regular Element #1',
+                        'image' => [],
+                    ],
+                    299 => [
+                        'header' => '[Translate to Dansk:] Regular Element #3',
+                        'image' => ['[Kasper] Image translated to Dansk', '[T3BOARD] Image added in Dansk (without parent)'],
+                    ],
+                    303 => [
+                        'header' => '[DK] Without default language',
+                        'image' => ['[T3BOARD] Image added to DK element without default language'],
+                    ],
+                ],
+                'fallbackType' => 'strict',
+                'fallbackChain' => 'pageNotFound',
+                'overlayMode' => 'includeFloating',
+            ],
+        ];
+    }
+
+    /**
+     * Page is translated to Dutch, so changing fallbackChain does not matter currently.
+     * Page title is always [DK]Page, the content language is always "1"
+     * @test
+     * @dataProvider dutchDataProvider
+     *
+     * @param array $languageConfiguration
+     * @param array $visibleRecords
+     * @param string $fallbackType
+     * @param string $fallbackChain
+     * @param string $overlayType
+     */
+    public function renderingOfDutchLanguage(array $languageConfiguration, array $visibleRecords, string $fallbackType, string $fallbackChain, string $overlayType)
+    {
+        $this->writeSiteConfiguration(
+            'test',
+            $this->buildSiteConfiguration(1, 'https://website.local/'),
+            [
+                $this->buildDefaultLanguageConfiguration('EN', '/en/'),
+                $this->buildLanguageConfiguration('DK', '/dk/', $languageConfiguration['fallbackChain'] ?? [], $languageConfiguration['fallbackType'])
+            ],
+            [
+                $this->buildErrorHandlingConfiguration('Fluid', [404])
+            ]
+        );
+
+        $response = $this->executeFrontendRequest(
+            new InternalRequest('https://website.local/dk/?id=' . static::VALUE_PageId)
+        );
+        $responseStructure = ResponseContent::fromString((string)$response->getBody());
+        $responseSections = $responseStructure->getSections();
+        $visibleHeaders = array_map(function ($element) {
+            return $element['header'];
+        }, $visibleRecords);
+
+        $this->assertThat(
+            $responseSections,
+            $this->getRequestSectionHasRecordConstraint()
+            ->setTable(self::TABLE_Content)
+            ->setField('header')
+            ->setValues(...$visibleHeaders)
+        );
+        $this->assertThat(
+            $responseSections,
+            $this->getRequestSectionDoesNotHaveRecordConstraint()
+            ->setTable(self::TABLE_Content)
+            ->setField('header')
+            ->setValues(...$this->getNonVisibleHeaders($visibleHeaders))
+        );
+
+        foreach ($visibleRecords as $ttContentUid => $properties) {
+            $visibleFileTitles = $properties['image'];
+            if (!empty($visibleFileTitles)) {
+                $this->assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
+                    ->setRecordIdentifier(self::TABLE_Content . ':' . $ttContentUid)->setRecordField('image')
+                    ->setTable('sys_file_reference')->setField('title')->setValues(...$visibleFileTitles));
+            }
+            $this->assertThat($responseSections, $this->getRequestSectionStructureDoesNotHaveRecordConstraint()
+                ->setRecordIdentifier(self::TABLE_Content . ':' . $ttContentUid)->setRecordField('image')
+                ->setTable('sys_file_reference')->setField('title')->setValues(...$this->getNonVisibleFileTitles($visibleFileTitles)));
+        }
+
+        $this->assertEquals('[DK]Page', $responseStructure->getScopePath('page/title'));
+        $this->assertEquals(1, $responseStructure->getScopePath('languageInfo/id'), 'languageId does not match');
+        $this->assertEquals(1, $responseStructure->getScopePath('languageInfo/contentId'), 'contentId does not match');
+        $this->assertEquals($fallbackType, $responseStructure->getScopePath('languageInfo/fallbackType'), 'fallbackType does not match');
+        $this->assertEquals($fallbackChain, $responseStructure->getScopePath('languageInfo/fallbackChain'), 'fallbackChain does not match');
+        $this->assertEquals($overlayType, $responseStructure->getScopePath('languageInfo/overlayType'), 'language overlayType does not match');
+    }
+
+    public function contentOnNonTranslatedPageDataProvider(): array
+    {
+        //Expected behaviour:
+        //the page is NOT translated so setting sys_language_mode to different values changes the results
+        //- setting sys_language_mode to empty value makes TYPO3 return default language records
+        //- setting it to strict throws 404, independently from other settings
+        //Setting config.sys_language_overlay = 0
+        return [
+            [
+                'languageConfiguration' => [
+                    'fallbackType' => 'free',
+                    'fallbackChain' => ['EN']
+                ],
+                'visibleRecords' => [
+                    297 => [
+                        'header' => 'Regular Element #1',
+                        'image' => ['T3BOARD'],
+                    ],
+                    298 => [
+                        'header' => 'Regular Element #2',
+                        'image' => ['Kasper2'],
+                    ],
+                    299 => [
+                        'header' => 'Regular Element #3',
+                        'image' => ['Kasper'],
+                    ],
+                ],
+                'pageTitle' => 'Default language Page',
+                'languageId' => 2,
+                'contentId' => 0,
+                'fallbackType' => 'free',
+                'fallbackChain' => '0,pageNotFound',
+                'overlayMode' => 'off',
+            ],
+            [
+                'languageConfiguration' => [
+                    'fallbackType' => 'free',
+                    'fallbackChain' => ['EN']
+                ],
+                'visibleRecords' => [
+                    297 => [
+                        'header' => 'Regular Element #1',
+                        'image' => ['T3BOARD'],
+                    ],
+                    298 => [
+                        'header' => 'Regular Element #2',
+                        'image' => ['Kasper2'],
+                    ],
+                    299 => [
+                        'header' => 'Regular Element #3',
+                        'image' => ['Kasper'],
+                    ],
+                ],
+                'pageTitle' => 'Default language Page',
+                'languageId' => 2,
+                'contentId' => 0,
+                'fallbackType' => 'free',
+                'fallbackChain' => '0,pageNotFound',
+                'overlayMode' => 'off',
+            ],
+            [
+                'languageConfiguration' => [
+                    'fallbackType' => 'free',
+                    'fallbackChain' => ['DK', 'EN']
+                ],
+                'visibleRecords' => [
+                    300 => [
+                        'header' => '[Translate to Dansk:] Regular Element #3',
+                        'image' => ['[Kasper] Image translated to Dansk', '[T3BOARD] Image added in Dansk (without parent)'],
+                    ],
+                    301 => [
+                        'header' => '[Translate to Dansk:] Regular Element #1',
+                        'image' => [],
+                    ],
+                    303 => [
+                        'header' => '[DK] Without default language',
+                        'image' => ['[T3BOARD] Image added to DK element without default language'],
+                    ],
+                    308 => [
+                        'header' => '[DK] UnHidden Element #4',
+                        'image' => [],
+                    ],
+                ],
+                'pageTitle' => '[DK]Page',
+                'languageId' => 2,
+                'contentId' => 1,
+                'fallbackType' => 'free',
+                'fallbackChain' => '1,0,pageNotFound',
+                'overlayMode' => 'off',
+            ],
+            [
+                'languageConfiguration' => [
+                    'fallbackType' => 'free'
+                ],
+                'visibleRecords' => [],
+                'pageTitle' => '',
+                'languageId' => 2,
+                'contentId' => 2,
+                'fallbackType' => 'free',
+                'fallbackChain' => 'pageNotFound',
+                'overlayMode' => 'off',
+                'statusCode' => 404,
+            ],
+            [
+                'languageConfiguration' => [
+                    'fallbackType' => 'fallback',
+                    'fallbackChain' => ['EN']
+                ],
+                'visibleRecords' => [
+                    297 => [
+                        'header' => 'Regular Element #1',
+                        'image' => ['T3BOARD'],
+                    ],
+                    298 => [
+                        'header' => 'Regular Element #2',
+                        'image' => ['Kasper2'],
+                    ],
+                    299 => [
+                        'header' => 'Regular Element #3',
+                        'image' => ['Kasper'],
+                    ],
+                ],
+                'pageTitle' => 'Default language Page',
+                'languageId' => 2,
+                'contentId' => 0,
+                'fallbackType' => 'fallback',
+                'fallbackChain' => '0,pageNotFound',
+                'overlayMode' => 'mixed',
+            ],
+            //falling back to default language
+            [
+                'languageConfiguration' => [
+                    'fallbackType' => 'fallback',
+                    'fallbackChain' => ['EN']
+                ],
+                'visibleRecords' => [
+                    297 => [
+                        'header' => 'Regular Element #1',
+                        'image' => ['T3BOARD'],
+                    ],
+                    298 => [
+                        'header' => 'Regular Element #2',
+                        'image' => ['Kasper2'],
+                    ],
+                    299 => [
+                        'header' => 'Regular Element #3',
+                        'image' => ['Kasper'],
+                    ],
+                ],
+                'pageTitle' => 'Default language Page',
+                'languageId' => 2,
+                'contentId' => 0,
+                'fallbackType' => 'fallback',
+                'fallbackChain' => '0,pageNotFound',
+                'overlayMode' => 'mixed',
+            ],
+            //Dutch elements are shown because of the content fallback 1,0 - first Dutch, then default language
+            //note that '[DK] Without default language' is NOT shown - due to overlays (fetch default language and overlay it with translations)
+            [
+                'languageConfiguration' => [
+                    'fallbackType' => 'fallback',
+                    'fallbackChain' => ['DK', 'EN']
+                ],
+                'visibleRecords' => [
+                    297 => [
+                        'header' => '[Translate to Dansk:] Regular Element #1',
+                        'image' => [],
+                    ],
+                    298 => [
+                        'header' => 'Regular Element #2',
+                        'image' => ['Kasper2'],
+                    ],
+                    299 => [
+                        'header' => '[Translate to Dansk:] Regular Element #3',
+                        'image' => ['[Kasper] Image translated to Dansk', '[T3BOARD] Image added in Dansk (without parent)'],
+                    ],
+                ],
+                'pageTitle' => '[DK]Page',
+                'languageId' => 2,
+                'contentId' => 1,
+                'fallbackType' => 'fallback',
+                'fallbackChain' => '1,0,pageNotFound',
+                'overlayMode' => 'mixed',
+            ],
+            [
+                'languageConfiguration' => [
+                    'fallbackType' => 'fallback',
+                    'fallbackChain' => []
+                ],
+                'visibleRecords' => [],
+                'pageTitle' => '',
+                'languageId' => 2,
+                'contentId' => 0,
+                'fallbackType' => 'fallback',
+                'fallbackChain' => 'pageNotFound',
+                'overlayMode' => 'mixed',
+                'statusCode' => 404
+            ],
+            [
+                'languageConfiguration' => [
+                    'fallbackType' => 'strict',
+                    'fallbackChain' => ['EN']
+                ],
+                'visibleRecords' => [
+                    297 => [
+                        'header' => 'Regular Element #1',
+                        'image' => ['T3BOARD'],
+                    ],
+                    298 => [
+                        'header' => 'Regular Element #2',
+                        'image' => ['Kasper2'],
+                    ],
+                    299 => [
+                        'header' => 'Regular Element #3',
+                        'image' => ['Kasper'],
+                    ],
+                ],
+                'pageTitle' => 'Default language Page',
+                'languageId' => 2,
+                'contentId' => 0,
+                'fallbackType' => 'strict',
+                'fallbackChain' => '0,pageNotFound',
+                'overlayMode' => 'includeFloating',
+            ],
+            [
+                'languageConfiguration' => [
+                    'fallbackType' => 'strict',
+                    'fallbackChain' => ['DK', 'EN']
+                ],
+                'visibleRecords' => [
+                    297 => [
+                        'header' => '[Translate to Dansk:] Regular Element #1',
+                        'image' => [],
+                    ],
+                    299 => [
+                        'header' => '[Translate to Dansk:] Regular Element #3',
+                        'image' => ['[Kasper] Image translated to Dansk', '[T3BOARD] Image added in Dansk (without parent)'],
+                    ],
+                    303 => [
+                        'header' => '[DK] Without default language',
+                        'image' => ['[T3BOARD] Image added to DK element without default language']
+                    ],
+                ],
+                'pageTitle' => '[DK]Page',
+                'languageId' => 2,
+                'contentId' => 1,
+                'fallbackType' => 'strict',
+                'fallbackChain' => '1,0,pageNotFound',
+                'overlayMode' => 'includeFloating',
+            ],
+            [
+                'languageConfiguration' => [
+                    'fallbackType' => 'strict',
+                    'fallbackChain' => []
+                ],
+                'visibleRecords' => [],
+                'pageTitle' => '',
+                'languageId' => 2,
+                'contentId' => 1,
+                'fallbackType' => 'strict',
+                'fallbackChain' => 'pageNotFound',
+                'overlayMode' => 'includeFloating',
+                'statusCode' => 404,
+            ],
+        ];
+    }
+
+    /**
+     * Page uid 89 is NOT translated to german
+     *
+     * @test
+     * @dataProvider contentOnNonTranslatedPageDataProvider
+     *
+     * @param array $languageConfiguration
+     * @param array $visibleRecords
+     * @param string $pageTitle
+     * @param int $languageId
+     * @param int $contentLanguageId
+     * @param string $fallbackType
+     * @param string fallbackkChain
+     * @param string $overlayMode
+     * @param int $statusCode 200 or 404
+     */
+    public function contentOnNonTranslatedPageGerman(array $languageConfiguration, array $visibleRecords, string $pageTitle, int $languageId, int $contentLanguageId, string $fallbackType, string $fallbackChain, string $overlayMode, int $statusCode = 200)
+    {
+        $this->writeSiteConfiguration(
+            'main',
+            $this->buildSiteConfiguration(1, 'https://website.local/'),
+            [
+                $this->buildDefaultLanguageConfiguration('EN', '/en/'),
+                $this->buildLanguageConfiguration('DK', '/dk/'),
+                $this->buildLanguageConfiguration('DE', '/de/', $languageConfiguration['fallbackChain'] ?? [], $languageConfiguration['fallbackType'])
+            ],
+            [
+                $this->buildErrorHandlingConfiguration('Fluid', [404])
+            ]
+        );
+
+        if ($statusCode === 404) {
+            $this->expectExceptionCode(1518472189);
+            $this->expectException(PageNotFoundException::class);
+        }
+        $response = $this->executeFrontendRequest(
+            new InternalRequest('https://website.local/de/?id=' . static::VALUE_PageId)
+        );
+
+        if ($statusCode === 200) {
+            $visibleHeaders = array_column($visibleRecords, 'header');
+            $responseStructure = ResponseContent::fromString((string)$response->getBody());
+            $responseSections = $responseStructure->getSections();
+
+            $this->assertThat(
+                $responseSections,
+                $this->getRequestSectionHasRecordConstraint()
+                ->setTable(self::TABLE_Content)
+                ->setField('header')
+                ->setValues(...$visibleHeaders)
+            );
+            $this->assertThat(
+                $responseSections,
+                $this->getRequestSectionDoesNotHaveRecordConstraint()
+                ->setTable(self::TABLE_Content)
+                ->setField('header')
+                ->setValues(...$this->getNonVisibleHeaders($visibleHeaders))
+            );
+
+            foreach ($visibleRecords as $ttContentUid => $properties) {
+                $visibleFileTitles = $properties['image'];
+                if (!empty($visibleFileTitles)) {
+                    $this->assertThat($responseSections, $this->getRequestSectionStructureHasRecordConstraint()
+                        ->setRecordIdentifier(self::TABLE_Content . ':' . $ttContentUid)->setRecordField('image')
+                        ->setTable('sys_file_reference')->setField('title')->setValues(...$visibleFileTitles));
+                }
+                $this->assertThat($responseSections, $this->getRequestSectionStructureDoesNotHaveRecordConstraint()
+                    ->setRecordIdentifier(self::TABLE_Content . ':' . $ttContentUid)->setRecordField('image')
+                    ->setTable('sys_file_reference')->setField('title')->setValues(...$this->getNonVisibleFileTitles($visibleFileTitles)));
+            }
+
+            $this->assertEquals($pageTitle, $responseStructure->getScopePath('page/title'));
+            $this->assertEquals($languageId, $responseStructure->getScopePath('languageInfo/id'), 'languageId does not match');
+            $this->assertEquals($contentLanguageId, $responseStructure->getScopePath('languageInfo/contentId'), 'contentId does not match');
+            $this->assertEquals($fallbackType, $responseStructure->getScopePath('languageInfo/fallbackType'), 'fallbackType does not match');
+            $this->assertEquals($fallbackChain, $responseStructure->getScopePath('languageInfo/fallbackChain'), 'fallbackChain does not match');
+            $this->assertEquals($overlayMode, $responseStructure->getScopePath('languageInfo/overlayType'), 'language overlayType does not match');
+        }
+    }
+
+    public function contentOnPartiallyTranslatedPageDataProvider(): array
+    {
+
+        //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 sys_language_content and sys_language_uid are always 3
+        return [
+            [
+                'languageConfiguration' => [
+                    'fallbackType' => 'free'
+                ],
+                'visibleRecordHeaders' => ['[Translate to Polski:] Regular Element #1', '[PL] Without default language'],
+                'fallbackType' => 'free',
+                'fallbackChain' => 'pageNotFound',
+                'overlayMode' => 'off',
+            ],
+            [
+                'languageConfiguration' => [
+                    'fallbackType' => 'free',
+                    'fallbackChain' => ['EN']
+                ],
+                'visibleRecordHeaders' => ['[Translate to Polski:] Regular Element #1', '[PL] Without default language'],
+                'fallbackType' => 'free',
+                'fallbackChain' => '0,pageNotFound',
+                'overlayMode' => 'off',
+            ],
+            [
+                'languageConfiguration' => [
+                    'fallbackType' => 'free',
+                    'fallbackChain' => ['DK', 'EN']
+                ],
+                '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)
+            [
+                'languageConfiguration' => [
+                    'fallbackType' => 'fallback',
+                    'fallbackChain' => ['EN']
+                ],
+                '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 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'],
+                'fallbackType' => 'fallback',
+                'fallbackChain' => '1,0,pageNotFound',
+                'overlayMode' => 'mixed',
+            ],
+            [
+                'languageConfiguration' => [
+                    'fallbackType' => 'fallback',
+                    'fallbackChain' => []
+                ],
+                '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
+            [
+                'languageConfiguration' => [
+                    'fallbackType' => 'strict',
+                    'fallbackChain' => ['EN']
+                ],
+                'visibleRecordHeaders' => ['[Translate to Polski:] Regular Element #1', '[PL] Without default language'],
+                'fallbackType' => 'strict',
+                'fallbackChain' => '0,pageNotFound',
+                'overlayMode' => 'includeFloating',
+            ],
+            [
+                'languageConfiguration' => [
+                    'fallbackType' => 'strict',
+                    'fallbackChain' => ['DK', 'EN']
+                ],
+                'visibleRecordHeaders' => ['[Translate to Polski:] Regular Element #1', '[PL] Without default language'],
+                'fallbackType' => 'strict',
+                'fallbackChain' => '1,0,pageNotFound',
+                'overlayMode' => 'includeFloating',
+            ],
+            [
+                'languageConfiguration' => [
+                    'fallbackType' => 'strict',
+                    '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
+     *
+     * @test
+     * @dataProvider contentOnPartiallyTranslatedPageDataProvider
+     *
+     * @param array $languageConfiguration
+     * @param array $visibleHeaders
+     * @param string $fallbackType
+     * @param string $fallbackChain
+     * @param string $overlayType
+     */
+    public function contentOnPartiallyTranslatedPage(array $languageConfiguration, array $visibleHeaders, string $fallbackType, string $fallbackChain, string $overlayType)
+    {
+        $this->writeSiteConfiguration(
+            'test',
+            $this->buildSiteConfiguration(1, 'https://website.local/'),
+            [
+                $this->buildDefaultLanguageConfiguration('EN', '/en/'),
+                $this->buildLanguageConfiguration('DK', '/dk/'),
+                $this->buildLanguageConfiguration('PL', '/pl/', $languageConfiguration['fallbackChain'] ?? [], $languageConfiguration['fallbackType'])
+            ],
+            [
+                $this->buildErrorHandlingConfiguration('Fluid', [404])
+            ]
+        );
+
+        $response = $this->executeFrontendRequest(
+            new InternalRequest('https://website.local/pl/?id=' . static::VALUE_PageId)
+        );
+        $responseStructure = ResponseContent::fromString((string)$response->getBody());
+        $responseSections = $responseStructure->getSections();
+
+        $this->assertEquals(200, $response->getStatusCode());
+
+        $this->assertThat(
+            $responseSections,
+            $this->getRequestSectionHasRecordConstraint()
+            ->setTable(self::TABLE_Content)
+            ->setField('header')
+            ->setValues(...$visibleHeaders)
+        );
+        $this->assertThat(
+            $responseSections,
+            $this->getRequestSectionDoesNotHaveRecordConstraint()
+            ->setTable(self::TABLE_Content)
+            ->setField('header')
+            ->setValues(...$this->getNonVisibleHeaders($visibleHeaders))
+        );
+
+        $this->assertEquals('[PL]Page', $responseStructure->getScopePath('page/title'));
+        $this->assertEquals(3, $responseStructure->getScopePath('languageInfo/id'), 'languageId does not match');
+        $this->assertEquals(3, $responseStructure->getScopePath('languageInfo/contentId'), 'contentId does not match');
+        $this->assertEquals($fallbackType, $responseStructure->getScopePath('languageInfo/fallbackType'), 'fallbackType does not match');
+        $this->assertEquals($fallbackChain, $responseStructure->getScopePath('languageInfo/fallbackChain'), 'fallbackChain does not match');
+        $this->assertEquals($overlayType, $responseStructure->getScopePath('languageInfo/overlayType'), 'language overlayType does not match');
+    }
+
+    /**
+     * Helper function to ease asserting that rest of the data set is not visible
+     *
+     * @param array $visibleHeaders
+     * @return array
+     */
+    protected function getNonVisibleHeaders(array $visibleHeaders): array
+    {
+        $allElements = [
+            'Regular Element #1',
+            'Regular Element #2',
+            'Regular Element #3',
+            'Hidden Element #4',
+            '[Translate to Dansk:] Regular Element #1',
+            '[Translate to Dansk:] Regular Element #3',
+            '[DK] Without default language',
+            '[DK] UnHidden Element #4',
+            '[DE] Without default language',
+            '[Translate to Deutsch:] [Translate to Dansk:] Regular Element #1',
+            '[Translate to Polski:] Regular Element #1',
+            '[PL] Without default language',
+            '[PL] Hidden Regular Element #2'
+        ];
+        return array_diff($allElements, $visibleHeaders);
+    }
+
+    /**
+     * Helper function to ease asserting that rest of the files are not present
+     *
+     * @param array $visibleTitles
+     * @return array
+     */
+    protected function getNonVisibleFileTitles(array $visibleTitles): array
+    {
+        $allElements = [
+            'T3BOARD',
+            'Kasper',
+            '[Kasper] Image translated to Dansk',
+            '[T3BOARD] Image added in Dansk (without parent)',
+            '[T3BOARD] Image added to DK element without default language',
+            '[T3BOARD] image translated to DE from DK',
+            'Kasper2',
+        ];
+        return array_diff($allElements, $visibleTitles);
+    }
+}