Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
82.26% covered (warning)
82.26%
51 / 62
42.86% covered (danger)
42.86%
3 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
LanguageFactory
83.61% covered (warning)
83.61%
51 / 61
42.86% covered (danger)
42.86%
3 / 7
26.54
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getLanguage
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 getLanguageCode
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getRawLanguage
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 newFromCode
91.67% covered (success)
91.67%
22 / 24
0.00% covered (danger)
0.00%
0 / 1
7.03
 classFromCode
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getParentLanguage
81.25% covered (warning)
81.25%
13 / 16
0.00% covered (danger)
0.00%
0 / 1
7.32
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Language;
8
9use InvalidArgumentException;
10use LogicException;
11use MediaWiki\Config\Config;
12use MediaWiki\Config\ServiceOptions;
13use MediaWiki\HookContainer\HookContainer;
14use MediaWiki\MainConfigNames;
15use MediaWiki\Title\NamespaceInfo;
16use Wikimedia\Bcp47Code\Bcp47Code;
17use Wikimedia\ObjectCache\MapCacheLRU;
18
19/**
20 * Internationalisation code
21 * See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more information.
22 *
23 * @ingroup Language
24 * @since 1.35
25 */
26class LanguageFactory {
27    /** @var MapCacheLRU */
28    private $langObjCache;
29
30    /** @var array */
31    private $parentLangCache = [];
32
33    /**
34     * @internal For use by ServiceWiring
35     */
36    public const CONSTRUCTOR_OPTIONS = [
37        MainConfigNames::DummyLanguageCodes,
38    ];
39
40    /** How many distinct Language objects to retain at most in memory (T40439). */
41    private const LANG_CACHE_SIZE = 10;
42
43    public function __construct(
44        private readonly ServiceOptions $options,
45        private readonly NamespaceInfo $namespaceInfo,
46        private readonly LocalisationCache $localisationCache,
47        private readonly LanguageNameUtils $langNameUtils,
48        private readonly LanguageFallback $langFallback,
49        private readonly LanguageConverterFactory $langConverterFactory,
50        private readonly HookContainer $hookContainer,
51        private readonly Config $config,
52        private readonly LeximorphFactory $leximorphFactory,
53    ) {
54        // We have both ServiceOptions and a Config object because
55        // the Language class hasn't (yet) been updated to use ServiceOptions
56        // and for now gets a full Config
57        $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
58
59        $this->langObjCache = new MapCacheLRU( self::LANG_CACHE_SIZE );
60    }
61
62    /**
63     * Get a cached or new language object for a given language code
64     * with normalization of the language code.
65     *
66     * If the language code comes from user input, check
67     * LanguageNameUtils::isValidCode() before calling this method.
68     *
69     * The language code is presumed to be a MediaWiki-internal code,
70     * unless you pass a Bcp47Code opaque object, in which case it is
71     * presumed to be a standard BCP-47 code.  (There are, regrettably,
72     * some ambiguous codes where this makes a difference.)
73     *
74     * As the Language class itself implements Bcp47Code, this method is an efficient
75     * and safe downcast if you pass in a Language object.
76     *
77     * @param string|Bcp47Code $code
78     * @return Language
79     */
80    public function getLanguage( $code ): Language {
81        if ( $code instanceof Language ) {
82            return $code;
83        }
84        if ( $code instanceof Bcp47Code ) {
85            // Any compatibility remapping of valid BCP-47 codes would be done
86            // inside ::bcp47ToInternal, not here.
87            $code = LanguageCode::bcp47ToInternal( $code );
88        } else {
89            // Perform various deprecated and compatibility mappings of
90            // internal codes.
91            $code = $this->options->get( MainConfigNames::DummyLanguageCodes )[$code] ?? $code;
92        }
93        return $this->getRawLanguage( $code );
94    }
95
96    public function getLanguageCode( string $code ): LanguageCode {
97        $code = $this->options->get( MainConfigNames::DummyLanguageCodes )[$code] ?? $code;
98        if ( !$this->langNameUtils->isValidCode( $code ) ) {
99            throw new InvalidArgumentException( "Invalid language code \"$code\"" );
100        }
101        return new LanguageCode( $code );
102    }
103
104    /**
105     * Get a cached or new language object for a given language code
106     * without normalization of the language code.
107     *
108     * If the language code comes from user input, check LanguageNameUtils::isValidCode()
109     * before calling this method.
110     *
111     * @param string $code
112     * @return Language
113     * @since 1.39
114     */
115    public function getRawLanguage( $code ): Language {
116        return $this->langObjCache->getWithSetCallback(
117            $code,
118            function () use ( $code ) {
119                return $this->newFromCode( $code );
120            }
121        );
122    }
123
124    /**
125     * Create a language object for a given language code.
126     *
127     * @param string $code
128     * @param bool $fallback Whether we're going through the language fallback chain
129     * @return Language
130     */
131    private function newFromCode( $code, $fallback = false ): Language {
132        if ( !$this->langNameUtils->isValidCode( $code ) ) {
133            throw new InvalidArgumentException( "Invalid language code \"$code\"" );
134        }
135
136        $constructorArgs = [
137            $code,
138            $this->namespaceInfo,
139            $this->localisationCache,
140            $this->langNameUtils,
141            $this->langFallback,
142            $this->langConverterFactory,
143            $this->hookContainer,
144            $this->config,
145            $this->leximorphFactory
146        ];
147
148        if ( !$this->langNameUtils->isValidBuiltInCode( $code ) ) {
149            // It's not possible to customise this code with class files, so
150            // just return a Language object. This is to support uselang= hacks.
151            return new Language( ...$constructorArgs );
152        }
153
154        // Check if there is a language class for the code
155        $class = $this->classFromCode( $code, $fallback );
156        // LanguageCode does not inherit Language
157        if ( class_exists( $class ) && is_a( $class, Language::class, true ) ) {
158            return new $class( ...$constructorArgs );
159        }
160
161        // Keep trying the fallback list until we find an existing class
162        $fallbacks = $this->langFallback->getAll( $code );
163        foreach ( $fallbacks as $fallbackCode ) {
164            $class = $this->classFromCode( $fallbackCode );
165            if ( class_exists( $class ) ) {
166                // TODO allow additional dependencies to be injected for subclasses somehow
167                return new $class( ...$constructorArgs );
168            }
169        }
170
171        throw new LogicException( "Invalid fallback sequence for language '$code'" );
172    }
173
174    /**
175     * @param string $code
176     * @param bool $fallback Whether we're going through the language fallback chain
177     * @return class-string<Language> Name of the language class
178     */
179    private function classFromCode( $code, $fallback = true ) {
180        if ( $fallback && $code == 'en' ) {
181            return Language::class;
182        } else {
183            return '\\MediaWiki\\Languages\\Language' . str_replace( '-', '_', ucfirst( $code ) );
184        }
185    }
186
187    /**
188     * Get the "parent" language which has a converter to convert a "compatible" language
189     * (in another variant) to this language (eg., zh for zh-cn, but not en for en-gb).
190     *
191     * @note This method does not contain the deprecated and compatibility
192     *  mappings of Language::getLanguage(string).
193     *
194     * @param string|Bcp47Code $code The language to convert to; can be an
195     *  internal MediaWiki language code or a Bcp47Code object (which includes
196     *  Language, which implements Bcp47Code).
197     * @return Language|null A base language which has a converter to the given
198     *  language, or null if none exists.
199     * @since 1.22
200     */
201    public function getParentLanguage( $code ) {
202        if ( $code instanceof Language ) {
203            $code = $code->getCode();
204        } elseif ( $code instanceof Bcp47Code ) {
205            $code = LanguageCode::bcp47ToInternal( $code );
206        }
207        // $code is now a mediawiki internal code string.
208        // We deliberately use array_key_exists() instead of isset() because we cache null.
209        if ( !array_key_exists( $code, $this->parentLangCache ) ) {
210            if ( !$this->langNameUtils->isValidBuiltInCode( $code ) ) {
211                $this->parentLangCache[$code] = null;
212                return null;
213            }
214            foreach ( LanguageConverter::$languagesWithVariants as $mainCode ) {
215                $lang = $this->getLanguage( $mainCode );
216                $converter = $this->langConverterFactory->getLanguageConverter( $lang );
217                if ( $converter->hasVariant( $code ) ) {
218                    $this->parentLangCache[$code] = $lang;
219                    return $lang;
220                }
221            }
222            $this->parentLangCache[$code] = null;
223        }
224
225        return $this->parentLangCache[$code];
226    }
227}
228
229/** @deprecated class alias since 1.45 */
230class_alias( LanguageFactory::class, 'MediaWiki\\Languages\\LanguageFactory' );