Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.62% covered (success)
90.62%
58 / 64
50.00% covered (danger)
50.00%
3 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
LanguageFactory
90.62% covered (success)
90.62%
58 / 64
50.00% covered (danger)
50.00%
3 / 6
22.40
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
10 / 10
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
 getRawLanguage
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 newFromCode
91.30% covered (success)
91.30%
21 / 23
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 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace MediaWiki\Languages;
22
23use InvalidArgumentException;
24use Language;
25use LanguageCode;
26use LanguageConverter;
27use LocalisationCache;
28use LogicException;
29use MapCacheLRU;
30use MediaWiki\Config\Config;
31use MediaWiki\Config\ServiceOptions;
32use MediaWiki\HookContainer\HookContainer;
33use MediaWiki\MainConfigNames;
34use MediaWiki\Title\NamespaceInfo;
35use Wikimedia\Bcp47Code\Bcp47Code;
36
37/**
38 * Internationalisation code
39 * See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more information.
40 *
41 * @ingroup Language
42 * @since 1.35
43 */
44class LanguageFactory {
45    /** @var ServiceOptions */
46    private $options;
47
48    /** @var NamespaceInfo */
49    private $namespaceInfo;
50
51    /** @var LocalisationCache */
52    private $localisationCache;
53
54    /** @var LanguageNameUtils */
55    private $langNameUtils;
56
57    /** @var LanguageFallback */
58    private $langFallback;
59
60    /** @var LanguageConverterFactory */
61    private $langConverterFactory;
62
63    /** @var HookContainer */
64    private $hookContainer;
65
66    /** @var MapCacheLRU */
67    private $langObjCache;
68
69    /** @var Config */
70    private $config;
71
72    /** @var array */
73    private $parentLangCache = [];
74
75    /**
76     * @internal For use by ServiceWiring
77     */
78    public const CONSTRUCTOR_OPTIONS = [
79        MainConfigNames::DummyLanguageCodes,
80    ];
81
82    /** How many distinct Language objects to retain at most in memory (T40439). */
83    private const LANG_CACHE_SIZE = 10;
84
85    /**
86     * @param ServiceOptions $options
87     * @param NamespaceInfo $namespaceInfo
88     * @param LocalisationCache $localisationCache
89     * @param LanguageNameUtils $langNameUtils
90     * @param LanguageFallback $langFallback
91     * @param LanguageConverterFactory $langConverterFactory
92     * @param HookContainer $hookContainer
93     * @param Config $config
94     */
95    public function __construct(
96        ServiceOptions $options,
97        NamespaceInfo $namespaceInfo,
98        LocalisationCache $localisationCache,
99        LanguageNameUtils $langNameUtils,
100        LanguageFallback $langFallback,
101        LanguageConverterFactory $langConverterFactory,
102        HookContainer $hookContainer,
103        Config $config
104    ) {
105        // We have both ServiceOptions and a Config object because
106        // the Language class hasn't (yet) been updated to use ServiceOptions
107        // and for now gets a full Config
108        $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
109
110        $this->options = $options;
111        $this->namespaceInfo = $namespaceInfo;
112        $this->localisationCache = $localisationCache;
113        $this->langNameUtils = $langNameUtils;
114        $this->langFallback = $langFallback;
115        $this->langConverterFactory = $langConverterFactory;
116        $this->hookContainer = $hookContainer;
117        $this->langObjCache = new MapCacheLRU( self::LANG_CACHE_SIZE );
118        $this->config = $config;
119    }
120
121    /**
122     * Get a cached or new language object for a given language code
123     * with normalization of the language code.
124     *
125     * If the language code comes from user input, check
126     * LanguageNameUtils::isValidCode() before calling this method.
127     *
128     * The language code is presumed to be a MediaWiki-internal code,
129     * unless you pass a Bcp47Code opaque object, in which case it is
130     * presumed to be a standard BCP-47 code.  (There are, regrettably,
131     * some ambiguous codes where this makes a difference.)
132     *
133     * As the Language class itself implements Bcp47Code, this method is an efficient
134     * and safe downcast if you pass in a Language object.
135     *
136     * @param string|Bcp47Code $code
137     * @return Language
138     */
139    public function getLanguage( $code ): Language {
140        if ( $code instanceof Language ) {
141            return $code;
142        }
143        if ( $code instanceof Bcp47Code ) {
144            // Any compatibility remapping of valid BCP-47 codes would be done
145            // inside ::bcp47ToInternal, not here.
146            $code = LanguageCode::bcp47ToInternal( $code );
147        } else {
148            // Perform various deprecated and compatibility mappings of
149            // internal codes.
150            $code = $this->options->get( MainConfigNames::DummyLanguageCodes )[$code] ?? $code;
151        }
152        return $this->getRawLanguage( $code );
153    }
154
155    /**
156     * Get a cached or new language object for a given language code
157     * without normalization of the language code.
158     *
159     * If the language code comes from user input, check LanguageNameUtils::isValidCode()
160     * before calling this method.
161     *
162     * @param string $code
163     * @return Language
164     * @since 1.39
165     */
166    public function getRawLanguage( $code ): Language {
167        return $this->langObjCache->getWithSetCallback(
168            $code,
169            function () use ( $code ) {
170                return $this->newFromCode( $code );
171            }
172        );
173    }
174
175    /**
176     * Create a language object for a given language code.
177     *
178     * @param string $code
179     * @param bool $fallback Whether we're going through the language fallback chain
180     * @return Language
181     */
182    private function newFromCode( $code, $fallback = false ): Language {
183        if ( !$this->langNameUtils->isValidCode( $code ) ) {
184            throw new InvalidArgumentException( "Invalid language code \"$code\"" );
185        }
186
187        $constructorArgs = [
188            $code,
189            $this->namespaceInfo,
190            $this->localisationCache,
191            $this->langNameUtils,
192            $this->langFallback,
193            $this->langConverterFactory,
194            $this->hookContainer,
195            $this->config
196        ];
197
198        if ( !$this->langNameUtils->isValidBuiltInCode( $code ) ) {
199            // It's not possible to customise this code with class files, so
200            // just return a Language object. This is to support uselang= hacks.
201            return new Language( ...$constructorArgs );
202        }
203
204        // Check if there is a language class for the code
205        $class = $this->classFromCode( $code, $fallback );
206        // LanguageCode does not inherit Language
207        if ( class_exists( $class ) && is_a( $class, 'Language', true ) ) {
208            return new $class( ...$constructorArgs );
209        }
210
211        // Keep trying the fallback list until we find an existing class
212        $fallbacks = $this->langFallback->getAll( $code );
213        foreach ( $fallbacks as $fallbackCode ) {
214            $class = $this->classFromCode( $fallbackCode );
215            if ( class_exists( $class ) ) {
216                // TODO allow additional dependencies to be injected for subclasses somehow
217                return new $class( ...$constructorArgs );
218            }
219        }
220
221        throw new LogicException( "Invalid fallback sequence for language '$code'" );
222    }
223
224    /**
225     * @param string $code
226     * @param bool $fallback Whether we're going through the language fallback chain
227     * @return string Name of the language class
228     */
229    private function classFromCode( $code, $fallback = true ) {
230        if ( $fallback && $code == 'en' ) {
231            return 'Language';
232        } else {
233            return 'Language' . str_replace( '-', '_', ucfirst( $code ) );
234        }
235    }
236
237    /**
238     * Get the "parent" language which has a converter to convert a "compatible" language
239     * (in another variant) to this language (eg., zh for zh-cn, but not en for en-gb).
240     *
241     * @note This method does not contain the deprecated and compatibility
242     *  mappings of Language::getLanguage(string).
243     *
244     * @param string|Bcp47Code $code The language to convert to; can be an
245     *  internal MediaWiki language code or a Bcp47Code object (which includes
246     *  Language, which implements Bcp47Code).
247     * @return Language|null A base language which has a converter to the given
248     *  language, or null if none exists.
249     * @since 1.22
250     */
251    public function getParentLanguage( $code ) {
252        if ( $code instanceof Language ) {
253            $code = $code->getCode();
254        } elseif ( $code instanceof Bcp47Code ) {
255            $code = LanguageCode::bcp47ToInternal( $code );
256        }
257        // $code is now a mediawiki internal code string.
258        // We deliberately use array_key_exists() instead of isset() because we cache null.
259        if ( !array_key_exists( $code, $this->parentLangCache ) ) {
260            if ( !$this->langNameUtils->isValidBuiltInCode( $code ) ) {
261                $this->parentLangCache[$code] = null;
262                return null;
263            }
264            foreach ( LanguageConverter::$languagesWithVariants as $mainCode ) {
265                $lang = $this->getLanguage( $mainCode );
266                $converter = $this->langConverterFactory->getLanguageConverter( $lang );
267                if ( $converter->hasVariant( $code ) ) {
268                    $this->parentLangCache[$code] = $lang;
269                    return $lang;
270                }
271            }
272            $this->parentLangCache[$code] = null;
273        }
274
275        return $this->parentLangCache[$code];
276    }
277}