Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 212
0.00% covered (danger)
0.00%
0 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
0.00% covered (danger)
0.00%
0 / 212
0.00% covered (danger)
0.00%
0 / 18
6972
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 setVersionConstant
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isEnabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isLanguageInHeader
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 isCompactLinksEnabled
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
90
 loadCodexStyles
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 onBeforePageDisplay
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
156
 handleSetLang
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 onSkinTemplateNavigation__Universal
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 addPersonalBarTrigger
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 getDefaultLanguage
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 onUserGetLanguageObject
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
110
 onResourceLoaderGetConfigVars
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
30
 onMakeGlobalVariablesScript
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 onGetPreferences
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
12
 onGetBetaFeaturePreferences
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
20
 onSkinAfterPortlet
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
90
 getSetLang
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * Hooks for UniversalLanguageSelector extension.
4 *
5 * Copyright (C) 2012-2018 Alolita Sharma, Amir Aharoni, Arun Ganesh, Brandon
6 * Harris, Niklas Laxström, Pau Giner, Santhosh Thottingal, Siebrand Mazeland
7 * and other contributors. See CREDITS for a list.
8 *
9 * UniversalLanguageSelector is dual licensed GPLv2 or later and MIT. You don't
10 * have to do anything special to choose one license or the other and you don't
11 * have to notify anyone which license you are using. You are free to use
12 * UniversalLanguageSelector in commercial projects as long as the copyright
13 * header is left intact. See files GPL-LICENSE and MIT-LICENSE for details.
14 *
15 * @file
16 * @ingroup Extensions
17 * @license GPL-2.0-or-later
18 * @license MIT
19 */
20
21namespace UniversalLanguageSelector;
22
23use Config;
24use ExtensionRegistry;
25use IBufferingStatsdDataFactory;
26use IContextSource;
27use LanguageCode;
28use MediaWiki\Babel\Babel;
29use MediaWiki\Extension\BetaFeatures\BetaFeatures;
30use MediaWiki\Hook\BeforePageDisplayHook;
31use MediaWiki\Hook\MakeGlobalVariablesScriptHook;
32use MediaWiki\Hook\UserGetLanguageObjectHook;
33use MediaWiki\Html\Html;
34use MediaWiki\Languages\LanguageNameUtils;
35use MediaWiki\Preferences\Hook\GetPreferencesHook;
36use MediaWiki\ResourceLoader\Hook\ResourceLoaderGetConfigVarsHook;
37use MediaWiki\Skins\Hook\SkinAfterPortletHook;
38use MediaWiki\User\UserOptionsLookup;
39use OutputPage;
40use RequestContext;
41use Skin;
42use SkinTemplate;
43use User;
44
45/**
46 * @phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName
47 */
48class Hooks implements
49    BeforePageDisplayHook,
50    UserGetLanguageObjectHook,
51    ResourceLoaderGetConfigVarsHook,
52    MakeGlobalVariablesScriptHook,
53    GetPreferencesHook,
54    SkinAfterPortletHook
55{
56
57    /** @var Config */
58    private $config;
59
60    /** @var UserOptionsLookup */
61    private $userOptionsLookup;
62
63    /** @var IBufferingStatsdDataFactory */
64    private $statsdDataFactory;
65
66    /** @var LanguageNameUtils */
67    private $languageNameUtils;
68
69    /**
70     * @param Config $config
71     * @param UserOptionsLookup $userOptionsLookup
72     * @param IBufferingStatsdDataFactory $statsdDataFactory
73     * @param LanguageNameUtils $languageNameUtils
74     */
75    public function __construct(
76        Config $config,
77        UserOptionsLookup $userOptionsLookup,
78        IBufferingStatsdDataFactory $statsdDataFactory,
79        LanguageNameUtils $languageNameUtils
80    ) {
81        $this->config = $config;
82        $this->userOptionsLookup = $userOptionsLookup;
83        $this->statsdDataFactory = $statsdDataFactory;
84        $this->languageNameUtils = $languageNameUtils;
85    }
86
87    public static function setVersionConstant() {
88        define( 'ULS_VERSION', '2020-07-20' );
89    }
90
91    /**
92     * Whether user visible ULS features are enabled (language changing, input methods, web
93     * fonts, language change undo tooltip).
94     * @return bool
95     */
96    private function isEnabled(): bool {
97        return (bool)$this->config->get( 'ULSEnable' );
98    }
99
100    /**
101     * Checks whether language is in header.
102     *
103     * @param Skin $skin
104     * @return bool
105     */
106    private function isLanguageInHeader( Skin $skin ): bool {
107        $languageInHeaderConfig = $skin->getConfig()->get( 'VectorLanguageInHeader' );
108        $userStatus = $skin->getUser()->isAnon() ? 'logged_out' : 'logged_in';
109        return $languageInHeaderConfig[ $userStatus ] ?? true;
110    }
111
112    /**
113     * Whether ULS Compact interlanguage links enabled
114     *
115     * @param User $user
116     * @param Skin $skin
117     * @return bool
118     */
119    private function isCompactLinksEnabled( User $user, Skin $skin ) {
120        // Whether any user visible features are enabled
121        if ( !$this->config->get( 'ULSEnable' ) ) {
122            return false;
123        }
124        // Compact links should be disabled in Vector 2022 skin,
125        // when the language button is displayed at the top of the content
126        if ( $skin->getSkinName() === 'vector-2022' ) {
127            return !$this->isLanguageInHeader( $skin );
128        }
129        if ( $this->config->get( 'ULSCompactLanguageLinksBetaFeature' ) === true &&
130            $this->config->get( 'InterwikiMagic' ) === true &&
131            $this->config->get( 'HideInterlanguageLinks' ) === false &&
132            ExtensionRegistry::getInstance()->isLoaded( 'BetaFeatures' ) &&
133            BetaFeatures::isFeatureEnabled( $user, 'uls-compact-links' )
134        ) {
135            // Compact language links is a beta feature in this wiki. Check the user's
136            // preference.
137            return true;
138        }
139
140        if ( $this->config->get( 'ULSCompactLanguageLinksBetaFeature' ) === false ) {
141            // Compact language links is a default feature in this wiki.
142            // Check user preference
143            return $this->userOptionsLookup
144                ->getBoolOption( $user, 'compact-language-links' );
145        }
146
147        return false;
148    }
149
150    /**
151     * Adds Codex styles in a way that is compatible with MLEB.
152     *
153     * @param OutputPage $out
154     */
155    private function loadCodexStyles( OutputPage $out ) {
156        if ( version_compare( MW_VERSION, '1.41', '<' ) ) {
157            // codex-search-styles was added in 1.41 so in older versions for MLEB support
158            // we load the full module.
159            $out->addModuleStyles( '@wikimedia/codex' );
160        } else {
161            // Only needed for skins that do not load Codex.
162            if ( !in_array( $out->getSkin()->getSkinName(), [ 'minerva', 'vector-2022' ] ) ) {
163                $out->addModuleStyles( 'codex-search-styles' );
164            }
165        }
166    }
167
168    /**
169     * @param OutputPage $out
170     * @param Skin $skin
171     * Hook: BeforePageDisplay
172     */
173    public function onBeforePageDisplay( $out, $skin ): void {
174        $unsupportedSkins = [ 'minerva', 'apioutput' ];
175        if ( in_array( $skin->getSkinName(), $unsupportedSkins, true ) ) {
176            return;
177        }
178        // Soft dependency to Wikibase client. Don't enable CLL if links are managed manually.
179        $excludedLinks = $out->getProperty( 'noexternallanglinks' );
180        $override = is_array( $excludedLinks ) && in_array( '*', $excludedLinks, true );
181        $isCompactLinksEnabled = $this->isCompactLinksEnabled( $out->getUser(), $skin );
182        $isVector2022LanguageInHeader = $skin->getSkinName() === 'vector-2022' && $this->isLanguageInHeader( $skin );
183        $config = [
184            'wgULSPosition' => $this->config->get( 'ULSPosition' ),
185            'wgULSisCompactLinksEnabled' => $isCompactLinksEnabled,
186            'wgVector2022LanguageInHeader' => $isVector2022LanguageInHeader
187        ];
188
189        if ( !$override && $isCompactLinksEnabled ) {
190            $out->addModules( 'ext.uls.compactlinks' );
191            // Add styles for the default button in the page.
192            $this->loadCodexStyles( $out );
193        }
194
195        if ( is_string( $this->config->get( 'ULSGeoService' ) ) ) {
196            $out->addModules( 'ext.uls.geoclient' );
197        }
198
199        if ( $this->isEnabled() ) {
200            // Enable UI language selection for the user.
201            $out->addModules( 'ext.uls.interface' );
202            $this->loadCodexStyles( $out );
203
204            $title = $out->getTitle();
205            $isMissingPage = !$title || !$title->exists();
206            // if current page doesn't exist or if it's a talk page, we should use a different layout inside ULS
207            // according to T316559. Add JS config variable here, to let frontend know, when this is the case
208            $config[ 'wgULSisLanguageSelectorEmpty' ] = $isMissingPage || $title->isTalkPage();
209        }
210
211        // This is added here, and not in onResourceLoaderGetConfigVars to allow skins and extensions
212        // to vary it. For example, ContentTranslation special pages depend on being able to change it.
213        $out->addJsConfigVars( $config );
214
215        if ( $this->config->get( 'ULSPosition' ) === 'personal' ) {
216            $out->addModuleStyles( 'ext.uls.pt' );
217        } else {
218            $out->addModuleStyles( 'ext.uls.interlanguage' );
219        }
220
221        if ( $out->getTitle()->isSpecial( 'Preferences' ) ) {
222            $out->addModuleStyles( 'ext.uls.preferencespage' );
223        }
224
225        $this->handleSetLang( $out );
226    }
227
228    /**
229     * Handle setlang query parameter; and decide if the setlang related scripts
230     * have to be loaded.
231     * @param OutputPage $out
232     * @return void
233     */
234    protected function handleSetLang( OutputPage $out ): void {
235        $languageToSet = $this->getSetLang( $out );
236
237        if ( !$languageToSet ) {
238            return;
239        }
240
241        $this->statsdDataFactory->increment( 'uls.setlang_used' );
242
243        $user = $out->getUser();
244        if ( !$user->isRegistered() && !$out->getConfig()->get( 'ULSAnonCanChangeLanguage' ) ) {
245            // User is anon, and cannot change language, return.
246            return;
247        }
248
249        $out->addModules( 'ext.uls.setlang' );
250    }
251
252    /**
253     * @param SkinTemplate $skin
254     * @param array &$links
255     */
256    public function onSkinTemplateNavigation__Universal( SkinTemplate $skin, array &$links ) {
257        // In modern skins which separate out the user menu,
258        // e.g. Vector. (T282196)
259        // this should appear in the `user-interface-preferences` menu.
260        // For older skins not separating out the user menu this will be prepended.
261        if ( isset( $links['user-interface-preferences'] ) ) {
262            $links['user-interface-preferences'] = $this->addPersonalBarTrigger(
263                $links['user-interface-preferences'],
264                $skin
265            );
266        }
267    }
268
269    /**
270     * Add some tabs for navigation for users who do not use Ajax interface.
271     * @param array &$personal_urls
272     * @param SkinTemplate $context SkinTemplate object providing context
273     * @return array of modified personal urls
274     */
275    private function addPersonalBarTrigger(
276        array &$personal_urls,
277        SkinTemplate $context
278    ) {
279        if ( $this->config->get( 'ULSPosition' ) !== 'personal' ) {
280            return $personal_urls;
281        }
282
283        if ( !$this->isEnabled() ) {
284            return $personal_urls;
285        }
286
287        // The element id will be 'pt-uls'
288        $mwLangCode = $context->getLanguage()->getCode();
289
290        return [
291            'uls' => [
292                'text' => $this->languageNameUtils->getLanguageName( $mwLangCode ),
293                'href' => '#',
294                // Skin meta data to allow skin (e.g. Vector) to add icons
295                'icon' => 'wikimedia-language',
296                // Skin meta data to allow skin (e.g. Vector) to convert to button.
297                'button' => true,
298                'link-class' => [ 'uls-trigger' ],
299                'active' => true
300            ]
301        ] + $personal_urls;
302    }
303
304    /**
305     * @param float[] $preferred Mapping of
306     *  'Preferred languages by lowercased BCP 47 language codes' => 'weight'
307     * @return string MediaWiki internal language code or empty string if there's no matched
308     *  language code
309     */
310    protected function getDefaultLanguage( array $preferred ) {
311        /** @var array supported List of Supported languages by MediaWiki internal language codes */
312        $supported = $this->languageNameUtils
313            ->getLanguageNames( LanguageNameUtils::AUTONYMS, LanguageNameUtils::SUPPORTED );
314
315        // Convert BCP 47 language code to MediaWiki internal language code and
316        // look for a MediaWiki internal language code that is acceptable to the client
317        // and known to the wiki.
318        foreach ( $preferred as $bcp47LangCode => $weight ) {
319            $mwLangCode = LanguageCode::bcp47ToInternal( $bcp47LangCode );
320            if ( isset( $supported[$mwLangCode] ) ) {
321                return $mwLangCode;
322            }
323        }
324
325        // Some browsers might:
326        // - Sent codes like 'zh-hant-tw':
327        //   FIXME: Try 'zh-tw', 'zh-hant', 'zh' respectively
328        // - Only send codes like 'de-de':
329        //   Try with bare code 'de'
330        foreach ( $preferred as $bcp47LangCode => $weight ) {
331            $parts = explode( '-', $bcp47LangCode, 2 );
332            $mwLangCode = $parts[0];
333            if ( isset( $supported[$mwLangCode] ) ) {
334                return $mwLangCode;
335            }
336        }
337
338        return '';
339    }
340
341    /**
342     * Hook to UserGetLanguageObject
343     * @param User $user
344     * @param string &$code
345     * @param IContextSource $context
346     */
347    public function onUserGetLanguageObject( $user, &$code, $context ) {
348        if ( $this->config->get( 'ULSLanguageDetection' ) ) {
349            // Vary any caching based on the header value. Note that
350            // we need to vary regardless of whether we end up using
351            // the header or not, so that requests without the header
352            // don't show up for people with it.
353            $context->getOutput()->addVaryHeader( 'Accept-Language' );
354        }
355
356        if ( !$this->isEnabled() ) {
357            return;
358        }
359
360        $request = $context->getRequest();
361
362        if (
363            // uselang can be used for temporary override of language preference
364            $request->getRawVal( 'uselang' ) ||
365            // Registered user: use preferences, only when safe to load - T267445
366            ( $user->isSafeToLoad() && $user->isRegistered() )
367        ) {
368            return;
369        }
370
371        // If using cookie storage for anons is OK, read from that
372        if ( $this->config->get( 'ULSAnonCanChangeLanguage' ) ) {
373            // Try to set the language based on the cookie
374            $languageToUse = $request->getCookie( 'language', null, '' );
375            if ( $this->languageNameUtils->isSupportedLanguage( $languageToUse ) ) {
376                $code = $languageToUse;
377
378                return;
379            }
380        }
381
382        // As last resort, try Accept-Language headers if allowed
383        if ( $this->config->get( 'ULSLanguageDetection' ) ) {
384            // We added a Vary header at the top of this function,
385            // since we're depending upon the Accept-Language header
386            $preferred = $request->getAcceptLang();
387            $default = $this->getDefaultLanguage( $preferred );
388            if ( $default !== '' ) {
389                $code = $default;
390            }
391        }
392    }
393
394    /**
395     * Hook: ResourceLoaderGetConfigVars
396     * @param array &$vars
397     * @param string $skin
398     * @param Config $config
399     */
400    public function onResourceLoaderGetConfigVars( array &$vars, $skin, Config $config ): void {
401        $extRegistry = ExtensionRegistry::getInstance();
402        $skinConfig = $extRegistry->getAttribute( 'UniversalLanguageSelectorSkinConfig' )[ $skin ] ?? [];
403        // Place constant stuff here (not depending on request context)
404
405        if ( is_string( $config->get( 'ULSGeoService' ) ) ) {
406            $vars['wgULSGeoService'] = $config->get( 'ULSGeoService' );
407        }
408
409        $vars['wgULSIMEEnabled'] = $config->get( 'ULSIMEEnabled' );
410        $vars['wgULSWebfontsEnabled'] = $config->get( 'ULSWebfontsEnabled' );
411        $vars['wgULSAnonCanChangeLanguage'] = $config->get( 'ULSAnonCanChangeLanguage' );
412        $vars['wgULSImeSelectors'] = $config->get( 'ULSImeSelectors' );
413        $vars['wgULSNoImeSelectors'] = $config->get( 'ULSNoImeSelectors' );
414        $vars['wgULSNoWebfontsSelectors'] = $config->get( 'ULSNoWebfontsSelectors' );
415        $vars['wgULSDisplaySettingsInInterlanguage'] = $skinConfig['ULSDisplaySettingsInInterlanguage'] ?? false;
416
417        if ( is_string( $config->get( 'ULSFontRepositoryBasePath' ) ) ) {
418            $vars['wgULSFontRepositoryBasePath'] = $config->get( 'ULSFontRepositoryBasePath' );
419        } else {
420            $vars['wgULSFontRepositoryBasePath'] = $config->get( 'ExtensionAssetsPath' ) .
421                '/UniversalLanguageSelector/data/fontrepo/fonts/';
422        }
423
424        if ( $config->has( 'InterwikiSortingSortPrepend' ) &&
425            $config->get( 'InterwikiSortingSortPrepend' ) !== []
426        ) {
427            $vars['wgULSCompactLinksPrepend'] = $config->get( 'InterwikiSortingSortPrepend' );
428        }
429    }
430
431    /**
432     * Hook: MakeGlobalVariablesScript
433     * @param array &$vars
434     * @param OutputPage $out
435     */
436    public function onMakeGlobalVariablesScript( &$vars, $out ): void {
437        // Place request context dependent stuff here
438        $user = $out->getUser();
439        $loggedIn = $user->isRegistered();
440
441        // Do not output accept languages if there is risk it will get cached across requests
442        if ( $out->getConfig()->get( 'ULSAnonCanChangeLanguage' ) || $loggedIn ) {
443            $vars['wgULSAcceptLanguageList'] = array_keys( $out->getRequest()->getAcceptLang() );
444        }
445
446        if ( $loggedIn && ExtensionRegistry::getInstance()->isLoaded( 'Babel' ) ) {
447            $userLanguageInfo = Babel::getCachedUserLanguageInfo( $user );
448
449            // This relies on the fact that Babel levels are 'N' and
450            // the digits 0 to 5 as strings, and that in reverse
451            // ASCII order they will be 'N', '5', '4', '3', '2', '1', '0'.
452            arsort( $userLanguageInfo );
453
454            $vars['wgULSBabelLanguages'] = array_keys( $userLanguageInfo );
455        }
456
457        // An optimization to avoid loading all of uls.data just to get the autonym
458        $langCode = $out->getLanguage()->getCode();
459        $vars['wgULSCurrentAutonym'] = $this->languageNameUtils->getLanguageName( $langCode );
460
461        $setLangCode = $this->getSetLang( $out );
462        if ( $setLangCode ) {
463            $vars['wgULSCurrentLangCode'] = $langCode;
464            $vars['wgULSSetLangCode'] = $setLangCode;
465            $vars['wgULSSetLangName'] = $this->languageNameUtils->getLanguageName( $setLangCode );
466        }
467    }
468
469    /**
470     * @param User $user User whose preferences are being modified
471     * @param array &$preferences Preferences description array, to be fed to an HTMLForm object
472     * @return bool|void True or no return value to continue or false to abort
473     */
474    public function onGetPreferences( $user, &$preferences ) {
475        // T259037: Does not work well on Minerva
476        $skin = RequestContext::getMain()->getSkin();
477        if ( $skin->getSkinName() === 'minerva' ) {
478            return;
479        }
480
481        $preferences['uls-preferences'] = [
482            'type' => 'api',
483        ];
484
485        // A link shown for accessing ULS language settings from preferences screen
486        $preferences['languagesettings'] = [
487            'type' => 'info',
488            'raw' => true,
489            'section' => 'personal/i18n',
490            // We use this class to hide this from no-JS users
491            'cssclass' => 'uls-preferences-link-wrapper',
492            'default' => "<a id='uls-preferences-link' class='uls-settings-trigger' role='button' tabindex='0'>" .
493                wfMessage( 'ext-uls-language-settings-preferences-link' )->escaped() . "</a>",
494        ];
495
496        if ( $this->config->get( 'ULSCompactLanguageLinksBetaFeature' ) === false ) {
497            $preferences['compact-language-links'] = [
498                'type' => 'check',
499                'section' => 'rendering/languages',
500                'label-message' => [
501                    'ext-uls-compact-language-links-preference',
502                    'mediawikiwiki:Special:MyLanguage/Universal_Language_Selector/Compact_Language_Links'
503                ]
504            ];
505        }
506    }
507
508    /**
509     * @param User $user
510     * @param array[] &$prefs
511     */
512    public function onGetBetaFeaturePreferences( $user, array &$prefs ) {
513        if ( $this->config->get( 'ULSCompactLanguageLinksBetaFeature' ) === true &&
514            $this->config->get( 'InterwikiMagic' ) === true &&
515            $this->config->get( 'HideInterlanguageLinks' ) === false
516        ) {
517            $extensionAssetsPath = $this->config->get( 'ExtensionAssetsPath' );
518            $imagesDir = "$extensionAssetsPath/UniversalLanguageSelector/resources/images";
519            $prefs['uls-compact-links'] = [
520                'label-message' => 'uls-betafeature-label',
521                'desc-message' => 'uls-betafeature-desc',
522                'screenshot' => [
523                    'ltr' => "$imagesDir/compact-links-ltr.svg",
524                    'rtl' => "$imagesDir/compact-links-rtl.svg",
525                ],
526                'info-link' =>
527                    'https://www.mediawiki.org/wiki/Special:MyLanguage/' .
528                    'Universal_Language_Selector/Compact_Language_Links',
529                'discussion-link' =>
530                    'https://www.mediawiki.org/wiki/Talk:Universal_Language_Selector/Compact_Language_Links',
531            ];
532        }
533    }
534
535    /**
536     * @param Skin $skin
537     * @param string $name
538     * @param string &$content
539     */
540    public function onSkinAfterPortlet( $skin, $name, &$content ) {
541        if ( $name !== 'lang' ) {
542            return;
543        }
544
545        // The ULS settings cog is only needed on projects which show the ULS button in the sidebar
546        // e.g. it is shown in the personal menu
547        if ( $this->config->get( 'ULSPosition' ) !== 'interlanguage' ) {
548            return;
549        }
550
551        $hasLanguages = $skin->getLanguages() !== [];
552        // For Vector 2022, the ULS settings cog is not needed for projects
553        // where a dedicated language button in the header ($wgVectorLanguageInHeader is true).
554        if ( $skin->getSkinName() === 'vector-2022' ) {
555            $languageInHeaderConfig = $skin->getConfig()->get( 'VectorLanguageInHeader' );
556            $languageInHeader = $languageInHeaderConfig[
557                $skin->getUser()->isAnon() ? 'logged_out' : 'logged_in' ] ?? true;
558            if ( $hasLanguages && $languageInHeader ) {
559                return;
560            }
561        }
562
563        if ( !$this->isEnabled() ) {
564            return;
565        }
566
567        // An empty span will force the language portal to always display in
568        // the skins that support it! e.g. Vector. (T275147)
569        if ( !$hasLanguages ) {
570            // If no languages force it on.
571            $content .= Html::element(
572                'span',
573                [ 'class' => 'uls-after-portlet-link', ],
574                ''
575            );
576        }
577    }
578
579    /**
580     * @param OutputPage $out
581     * @return string|null
582     */
583    private function getSetLang( OutputPage $out ): ?string {
584        $setLangCode = $out->getRequest()->getRawVal( 'setlang' );
585        if ( $setLangCode && $this->languageNameUtils->isSupportedLanguage( $setLangCode ) ) {
586            return $setLangCode;
587        }
588
589        return null;
590    }
591}