• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

FluidTYPO3 / vhs / 29999067194

23 Jul 2026 10:22AM UTC coverage: 70.309% (-0.5%) from 70.857%
29999067194

push

github

NamelessCoder
[FEATURE] Declare support for TYPO3v14

6 of 8 new or added lines in 3 files covered. (75.0%)

50 existing lines in 2 files now uncovered.

4819 of 6854 relevant lines covered (70.31%)

6.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

24.73
/Classes/ViewHelpers/Page/LanguageMenuViewHelper.php
1
<?php
2
namespace FluidTYPO3\Vhs\ViewHelpers\Page;
3

4
/*
5
 * This file is part of the FluidTYPO3/Vhs project under GPLv2 or later.
6
 *
7
 * For the full copyright and license information, please read the
8
 * LICENSE.md file that was distributed with this source code.
9
 */
10

11
use FluidTYPO3\Vhs\Proxy\DoctrineQueryProxy;
12
use FluidTYPO3\Vhs\Proxy\SiteFinderProxy;
13
use FluidTYPO3\Vhs\Traits\ArrayConsumingViewHelperTrait;
14
use FluidTYPO3\Vhs\Traits\TagViewHelperCompatibility;
15
use FluidTYPO3\Vhs\Utility\ContentObjectFetcher;
16
use FluidTYPO3\Vhs\Utility\CoreUtility;
17
use FluidTYPO3\Vhs\Utility\RequestResolver;
18
use FluidTYPO3\Vhs\Utility\VersionUtility;
19
use TYPO3\CMS\Core\Context\Context;
20
use TYPO3\CMS\Core\Context\LanguageAspect;
21
use TYPO3\CMS\Core\Database\ConnectionPool;
22
use TYPO3\CMS\Core\Imaging\Icon;
23
use TYPO3\CMS\Core\Imaging\IconFactory;
24
use TYPO3\CMS\Core\Imaging\IconSize;
25
use TYPO3\CMS\Core\Site\Site;
26
use TYPO3\CMS\Core\Utility\GeneralUtility;
27
use TYPO3\CMS\Core\Utility\VersionNumberUtility;
28
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
29
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
30
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
31
use TYPO3Fluid\Fluid\Core\ViewHelper\Exception;
32

33
/**
34
 * ViewHelper for rendering TYPO3 menus in Fluid
35
 * Require the extension static_info_table.
36
 */
37
class LanguageMenuViewHelper extends AbstractTagBasedViewHelper
38
{
39
    use ArrayConsumingViewHelperTrait;
40
    use TagViewHelperCompatibility;
41

42
    protected array $languageMenu = [];
43
    protected int $defaultLangUid = 0;
44

45
    /**
46
     * @var string
47
     */
48
    protected $tagName = 'ul';
49

50
    /**
51
     * @var ContentObjectRenderer
52
     */
53
    protected $cObj;
54

55
    protected ConfigurationManagerInterface $configurationManager;
56

57
    /**
58
     * @var Site|\TYPO3\CMS\Core\Site\Entity\Site
59
     */
60
    protected $site;
61

62
    public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager): void
63
    {
64
        $this->configurationManager = $configurationManager;
6✔
65
    }
66

67
    public function initializeArguments(): void
68
    {
69
        parent::initializeArguments();
3✔
70
        $this->registerUniversalTagAttributes();
3✔
71
        $this->registerArgument(
3✔
72
            'tagName',
3✔
73
            'string',
3✔
74
            'Tag name to use for enclosing container, list and flags (not finished) only',
3✔
75
            false,
3✔
76
            'ul'
3✔
77
        );
3✔
78
        $this->registerArgument(
3✔
79
            'tagNameChildren',
3✔
80
            'string',
3✔
81
            'Tag name to use for child nodes surrounding links, list and flags only',
3✔
82
            false,
3✔
83
            'li'
3✔
84
        );
3✔
85
        $this->registerArgument('defaultIsoFlag', 'string', 'ISO code of the default flag');
3✔
86
        $this->registerArgument('defaultLanguageLabel', 'string', 'Label for the default language');
3✔
87
        $this->registerArgument('order', 'mixed', 'Orders the languageIds after this list', false, '');
3✔
88
        $this->registerArgument('labelOverwrite', 'mixed', 'Overrides language labels');
3✔
89
        $this->registerArgument(
3✔
90
            'hideNotTranslated',
3✔
91
            'boolean',
3✔
92
            'Hides languageIDs which are not translated',
3✔
93
            false,
3✔
94
            false
3✔
95
        );
3✔
96
        $this->registerArgument(
3✔
97
            'layout',
3✔
98
            'string',
3✔
99
            'How to render links when using autorendering. Possible selections: name,flag - use fx "name" or ' .
3✔
100
            '"flag,name" or "name,flag"',
3✔
101
            false,
3✔
102
            'flag,name'
3✔
103
        );
3✔
104
        $this->registerArgument(
3✔
105
            'useCHash',
3✔
106
            'boolean',
3✔
107
            'Use cHash for typolink. Has no effect on TYPO3 v9.5+',
3✔
108
            false,
3✔
109
            true
3✔
110
        );
3✔
111
        $this->registerArgument('flagPath', 'string', 'Overwrites the path to the flag folder', false, '');
3✔
112
        $this->registerArgument('flagImageType', 'string', 'Sets type of flag image: png, gif, jpeg', false, 'svg');
3✔
113
        $this->registerArgument('linkCurrent', 'boolean', 'Sets flag to link current language or not', false, true);
3✔
114
        $this->registerArgument(
3✔
115
            'classCurrent',
3✔
116
            'string',
3✔
117
            'Sets the class, by which the current language will be marked',
3✔
118
            false,
3✔
119
            'current'
3✔
120
        );
3✔
121
        $this->registerArgument(
3✔
122
            'as',
3✔
123
            'string',
3✔
124
            'If used, stores the menu pages as an array in a variable named according to this value and renders ' .
3✔
125
            'the tag content - which means automatic rendering is disabled if this attribute is used',
3✔
126
            false,
3✔
127
            'languageMenu'
3✔
128
        );
3✔
129
        $this->registerArgument('pageUid', 'integer', 'Optional page uid to use.', false, 0);
3✔
130
        $this->registerArgument('configuration', 'array', 'Additional typoLink configuration', false, []);
3✔
131
        $this->registerArgument('excludeQueryVars', 'string', 'Comma-separate list of variables to exclude', false, '');
3✔
132
        $this->registerArgument(
3✔
133
            'languages',
3✔
134
            'mixed',
3✔
135
            'Array, CSV or Traversable containing UIDs of languages to render'
3✔
136
        );
3✔
137
    }
138

139
    public function render(): string
140
    {
141
        if (!is_object($GLOBALS['TSFE']->sys_page)) {
×
142
            return '';
×
143
        }
144
        /** @var ContentObjectRenderer|null $contentObject */
145
        $contentObject = ContentObjectFetcher::resolve($this->configurationManager);
×
146
        if ($contentObject === null) {
×
147
            throw new Exception('v:page.languageMenu requires a ContentObjectRenderer, none found', 1737807859);
×
148
        }
149

150
        $this->cObj = $contentObject;
×
151
        $this->tagName = is_scalar($this->arguments['tagName']) ? (string) $this->arguments['tagName'] : 'ul';
×
152
        $this->tag->setTagName($this->tagName);
×
153
        $this->site = $this->getSite();
×
154
        $this->defaultLangUid = $this->site->getDefaultLanguage()->getLanguageId();
×
155
        $this->languageMenu = $this->parseLanguageMenu();
×
156

157
        /** @var string $as */
158
        $as = $this->arguments['as'];
×
159
        $this->renderingContext->getVariableProvider()->add($as, $this->languageMenu);
×
160
        /** @var string|null $content */
161
        $content = $this->renderChildren();
×
162
        $content = is_scalar($content) ? (string) $content : '';
×
163
        $this->renderingContext->getVariableProvider()->remove($as);
×
164
        if (0 === mb_strlen(trim($content))) {
×
165
            $content = $this->autoRender();
×
166
        }
167
        return $content;
×
168
    }
169

170
    protected function autoRender(): string
171
    {
172
        $content = $this->getLanguageMenu();
×
173
        $content = trim($content);
×
174
        if (!empty($content)) {
×
175
            $this->tag->setContent($content);
×
176
            $content = $this->tag->render();
×
177
        }
178
        return $content;
×
179
    }
180

181
    /**
182
     * Get layout 0 (default): list
183
     */
184
    protected function getLanguageMenu(): string
185
    {
186
        $tagName = $this->arguments['tagNameChildren'];
×
187
        $html = [];
×
188
        $itemCount = count($this->languageMenu);
×
189
        foreach ($this->languageMenu as $index => $var) {
×
190
            $class = '';
×
191
            $classes = [];
×
192
            if ($var['inactive']) {
×
193
                $classes[] = 'inactive';
×
194
            }
195
            if ($var['current']) {
×
196
                $classes[] = $this->arguments['classCurrent'];
×
197
            }
198
            if (0 === $index) {
×
199
                $classes[] = 'first';
×
200
            } elseif (($itemCount - 1) === $index) {
×
201
                $classes[] = 'last';
×
202
            }
203
            if (0 < count($classes)) {
×
204
                $class = ' class="' . implode(' ', $classes) . '" ';
×
205
            }
206
            if ($var['current'] && !$this->arguments['linkCurrent']) {
×
207
                $html[] = '<' . $tagName . $class . '>' . $this->getLayout($var) . '</' . $tagName . '>';
×
208
            } else {
209
                $html[] = '<' . $tagName . $class . '><a href="' . htmlspecialchars($var['url']) . '">' .
×
210
                    $this->getLayout($var) . '</a></' . $tagName . '>';
×
211
            }
212
        }
213
        return implode(PHP_EOL, $html);
×
214
    }
215

216
    /**
217
     * Returns the flag source given the language ISO code.
218
     */
219
    protected function getLanguageFlag(string $iso, string $label): string
220
    {
221
        /** @var string $flagPath */
222
        $flagPath = $this->arguments['flagPath'];
×
223
        /** @var string $flagImageType */
224
        $flagImageType = $this->arguments['flagImageType'];
×
225
        if ('' !== $flagPath) {
×
226
            $path = trim($flagPath);
×
227
        } else {
228
            $path = CoreUtility::getLanguageFlagIconPath();
×
229
        }
230

231
        $imgType = trim($flagImageType);
×
232
        $conf = [
×
233
            'file' => $path . strtoupper($iso) . '.' . $imgType,
×
234
            'altText' => $label,
×
235
            'titleText' => $label
×
236
        ];
×
237

238
        if (file_exists($conf['file'])) {
×
239
            $contentObjectDefinition = $this->cObj->getContentObject('IMAGE');
×
240
            if ($contentObjectDefinition === null) {
×
241
                return '';
×
242
            }
243
            return $this->cObj->render($contentObjectDefinition, $conf);
×
244
        }
245
        return '';
×
246
    }
247

248
    /**
249
     * Returns the flag source given a TYPO3 icon identifier.
250
     */
251
    protected function getLanguageFlagByIdentifier(string $identifier): string
252
    {
NEW
253
        $iconEnum = VersionUtility::isCoreAtLeast14() ? IconSize::SMALL : Icon::SIZE_SMALL;
×
254
        /** @var IconFactory $iconFactory */
255
        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
×
NEW
256
        $icon = $iconFactory->getIcon($identifier, $iconEnum);
×
257
        return $icon->render();
×
258
    }
259

260
    /**
261
     * Return the layout: flag & text, flags only or text only.
262
     */
263
    protected function getLayout(array $language): string
264
    {
265
        /** @var string $layout */
266
        $layout = $this->arguments['layout'];
×
267
        /** @var string $flagCode */
268
        $flagCode = $language['flagCode'];
×
269
        $flagImage = false !== stripos($layout, 'flag') ? $flagCode : '';
×
270
        /** @var string $label */
271
        $label = $language['label'];
×
272
        switch ($this->arguments['layout']) {
×
273
            case 'flag':
×
274
                $html = $flagImage;
×
275
                break;
×
276
            case 'name':
×
277
                $html = $label;
×
278
                break;
×
279
            case 'name,flag':
×
280
                $html = $label;
×
281
                if ('' !== $flagImage) {
×
282
                    $html .= '&nbsp;' . $flagImage;
×
283
                }
284
                break;
×
285
            case 'flag,name':
×
286
            default:
287
                if ('' !== $flagImage) {
×
288
                    $html = $flagImage . '&nbsp;' . $label;
×
289
                } else {
290
                    $html = $label;
×
291
                }
292
        }
293
        return $html;
×
294
    }
295

296
    /**
297
     * Sets all parameter for langMenu.
298
     */
299
    protected function parseLanguageMenu(): array
300
    {
301
        /** @var array $languages */
302
        $languages = $this->arguments['languages'];
×
303
        /** @var string|null $orderArgument */
304
        $orderArgument = $this->arguments['order'];
×
305
        /** @var iterable $order */
306
        $order = $orderArgument ? GeneralUtility::trimExplode(',', $orderArgument) : '';
×
307
        /** @var string $labelOverwrite */
308
        $labelOverwrite = $this->arguments['labelOverwrite'];
×
309
        if (!empty($labelOverwrite)) {
×
310
            /** @var array $labelOverwrite */
311
            $labelOverwrite = GeneralUtility::trimExplode(',', $labelOverwrite);
×
312
        }
313

314
        // first gather languages into this array so we can reorder it later
315
        $limitLanguages = static::arrayFromArrayOrTraversableOrCSVStatic($languages ?? []);
×
316
        $limitLanguages = array_filter($limitLanguages);
×
317
        $tempArray = $this->getLanguagesFromSiteConfiguration($limitLanguages);
×
318

319
        // reorder languageMenu
320
        $languageMenu = [];
×
321
        if (!empty($order)) {
×
322
            foreach ($order as $value) {
×
323
                if (isset($tempArray[$value])) {
×
324
                    $languageMenu[$value] = $tempArray[$value];
×
325
                }
326
            }
327
        } else {
328
            $languageMenu = $tempArray;
×
329
        }
330

331
        // overwrite of label
332
        if (!empty($labelOverwrite)) {
×
333
            $i = 0;
×
334
            foreach ($languageMenu as $key => $value) {
×
335
                $languageMenu[$key]['label'] = $labelOverwrite[$i];
×
336
                $i++;
×
337
            }
338
        }
339

340
        // get the languages actually available on this page
341
        $languageUids = $this->getSystemLanguageUids();
×
342

343
        if (class_exists(LanguageAspect::class)) {
×
344
            /** @var Context $context */
345
            $context = GeneralUtility::makeInstance(Context::class);
×
346
            /** @var LanguageAspect $languageAspect */
347
            $languageAspect = $context->getAspect('language');
×
348
            $languageUid = $languageAspect->getId();
×
349
        } else {
350
            $languageUid = $GLOBALS['TSFE']->sys_language_uid;
×
351
        }
352

353
        foreach ($languageMenu as $key => $value) {
×
354
            $current = $languageUid === (int) $key ? 1 : 0;
×
355
            $inactive = in_array($key, $languageUids) || (int) $key === $this->defaultLangUid ? 0 : 1;
×
356
            $url = $this->getLanguageUrl($key);
×
357
            if (empty($url)) {
×
358
                $url = (string) RequestResolver::getRequest()->getUri();
×
359
            }
360
            $languageMenu[$key]['current'] = $current;
×
361
            $languageMenu[$key]['inactive'] = $inactive;
×
362
            $languageMenu[$key]['url'] = $url;
×
363
            $languageMenu[$key]['flagSrc'] = $this->getLanguageFlag($value['flag'] ?? $value['iso'], $value['label']);
×
364
            // if the user has set a flag path, always use that over the TYPO3 icon factory so the user
365
            // has the option to use custom flag images based on the ISO code of the language.
366
            // if the user has not set a flag path, prefer the TYPO3 icon factory when an icon
367
            // identifier is available (i.e., when using the site-based language lookup) .
368
            if (isset($value['flagIdentifier']) && empty($this->arguments['flagPath'])) {
×
369
                $languageMenu[$key]['flagCode'] = $this->getLanguageFlagByIdentifier($value['flagIdentifier']);
×
370
            } else {
371
                $languageMenu[$key]['flagCode'] = $this->getLanguageFlag(
×
372
                    $value['flag'] ?? $value['iso'],
×
373
                    $value['label']
×
374
                );
×
375
            }
376
            if ($this->arguments['hideNotTranslated'] && $inactive) {
×
377
                unset($languageMenu[$key]);
×
378
            }
379
        }
380

381
        return $languageMenu;
×
382
    }
383

384
    /**
385
     * Get the list of languages from the sys_language table.
386
     */
387
    protected function getLanguagesFromSysLanguage(array $limitLanguages): array
388
    {
389
        // add default language
390
        $result[0] = [
×
391
            'label' => $this->arguments['defaultLanguageLabel'] ?? 'English',
×
392
            'flag' => $this->arguments['defaultIsoFlag'] ?? 'gb'
×
393
        ];
×
394

395
        $select = 'uid,title,flag';
×
396
        $from = 'sys_language';
×
397

398
        if (!empty($limitLanguages)) {
×
399
            $sysLanguage = $GLOBALS['TSFE']->cObj->getRecords(
×
400
                $from,
×
401
                ['selectFields' => $select, 'pidInList' => 'root', 'uidInList' => implode(',', $limitLanguages)]
×
402
            );
×
403
        } else {
404
            $sysLanguage = $GLOBALS['TSFE']->cObj->getRecords(
×
405
                $from,
×
406
                ['selectFields' => $select, 'pidInList' => 'root']
×
407
            );
×
408
        }
409

410
        foreach ($sysLanguage as $value) {
×
411
            $result[$value['uid']] = [
×
412
                'label' => $value['title'],
×
413
                'flag' => $value['flag'],
×
414
            ];
×
415
        }
416

417
        return $result;
×
418
    }
419

420
    /**
421
     * Get the list of languages from the site configuration.
422
     */
423
    protected function getLanguagesFromSiteConfiguration(array $limitLanguages): array
424
    {
425
        $site = $this->getSite();
×
426
        // get only languages set as visible in frontend
427
        $languages = $site->getLanguages();
×
428
        $defaultLanguage = $site->getDefaultLanguage();
×
429

430
        $result = [];
×
431
        foreach ($languages as $language) {
×
432
            if (!empty($limitLanguages) && !in_array($language->getLanguageId(), $limitLanguages)) {
×
433
                continue;
×
434
            }
435
            $label = $language->getNavigationTitle();
×
436
            $flag = $language->getFlagIdentifier();
×
437
            if ($language->getLanguageId() == $defaultLanguage->getLanguageId()) {
×
438
                // override label/flag of default language if given
439
                $label = $this->arguments['defaultLanguageLabel'] ?? $label;
×
440
                $flag = $this->arguments['defaultIsoFlag'] ?? $flag;
×
441
            }
442
            if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '12.4', '>=')) {
×
443
                $isoCode = $language->getLocale()->getLanguageCode();
×
444
            } else {
445
                $isoCode = $language->getTwoLetterIsoCode();
×
446
            }
447
            $result[$language->getLanguageId()] = [
×
448
                'label' => $label,
×
449
                'iso' => $isoCode,
×
450
                'flagIdentifier' => $flag
×
451
            ];
×
452
        }
453

454
        return $result;
×
455
    }
456

457
    /**
458
     * Get link of language menu entry
459
     *
460
     * @param int|string $languageId
461
     */
462
    protected function getLanguageUrl($languageId): string
463
    {
464
        /** @var string $excludeVarsArgument */
465
        $excludeVarsArgument = $this->arguments['excludeQueryVars'];
×
466
        $excludedVars = trim((string) $excludeVarsArgument);
×
467
        $config = [
×
468
            'parameter' => $this->getPageUid(),
×
469
            'returnLast' => 'url',
×
470
            'additionalParams' => '&L=' . $languageId,
×
471
            'addQueryString' => 1,
×
472
            'addQueryString.' => [
×
473
                'method' => 'GET',
×
474
                'exclude' => 'id,L,cHash' . ($excludedVars ? ',' . $excludedVars : '')
×
475
            ]
×
476
        ];
×
477
        if (is_array($this->arguments['configuration'])) {
×
478
            $config = $this->mergeArrays($config, $this->arguments['configuration']);
×
479
        }
480
        return $this->cObj->typoLink('', $config);
×
481
    }
482

483
    /**
484
     * Get page via pageUid argument or current id
485
     */
486
    protected function getPageUid(): int
487
    {
488
        /** @var int $pageUid */
489
        $pageUid = $this->arguments['pageUid'];
×
490
        $pageUid = (int) $pageUid;
×
491
        if (0 === $pageUid) {
×
492
            $pageUid = $GLOBALS['TSFE']->id;
×
493
        }
494

495
        return (int) $pageUid;
×
496
    }
497

498
    /**
499
     * Find the site corresponding to the page that the menu is being rendered for
500
     *
501
     * @return Site|\TYPO3\CMS\Core\Site\Entity\Site
502
     */
503
    protected function getSite()
504
    {
505
        /** @var SiteFinderProxy $siteFinder */
506
        $siteFinder = GeneralUtility::makeInstance(SiteFinderProxy::class);
×
507
        return $siteFinder->getSiteByPageId($this->getPageUid());
×
508
    }
509

510
    /**
511
     * Fetches system languages available on the page depending on the TYPO3 version.
512
     *
513
     * @return int[]
514
     * @phpcsSuppress
515
     * @see https://docs.typo3.org/typo3cms/extensions/core/Changelog/9.0/Important-82445-MigratePagesLanguageOverlayIntoPages.html
516
     */
517
    protected function getSystemLanguageUids(): array
518
    {
519
        $table = 'pages';
×
520
        $parentField = 'l10n_parent';
×
521

522
        /** @var ConnectionPool $connectionPool */
523
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
×
524
        $connection = $connectionPool->getConnectionForTable($table);
×
525
        $queryBuilder = $connection->createQueryBuilder();
×
526
        $queryBuilder->select('sys_language_uid')
×
527
            ->from($table)
×
528
            ->where(
×
529
                $queryBuilder->expr()->eq($parentField, $this->getPageUid())
×
530
            );
×
531
        $result = DoctrineQueryProxy::executeQueryOnQueryBuilder($queryBuilder);
×
532
        $rows = DoctrineQueryProxy::fetchAllAssociative($result);
×
533

534
        return array_column($rows, 'sys_language_uid');
×
535
    }
536
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc