Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
82.26% |
51 / 62 |
|
42.86% |
3 / 7 |
CRAP | |
0.00% |
0 / 1 |
| LanguageFactory | |
83.61% |
51 / 61 |
|
42.86% |
3 / 7 |
26.54 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| getLanguage | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
3.04 | |||
| getLanguageCode | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| getRawLanguage | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
| newFromCode | |
91.67% |
22 / 24 |
|
0.00% |
0 / 1 |
7.03 | |||
| classFromCode | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
| getParentLanguage | |
81.25% |
13 / 16 |
|
0.00% |
0 / 1 |
7.32 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * @license GPL-2.0-or-later |
| 4 | * @file |
| 5 | */ |
| 6 | |
| 7 | namespace MediaWiki\Language; |
| 8 | |
| 9 | use InvalidArgumentException; |
| 10 | use LogicException; |
| 11 | use MediaWiki\Config\Config; |
| 12 | use MediaWiki\Config\ServiceOptions; |
| 13 | use MediaWiki\HookContainer\HookContainer; |
| 14 | use MediaWiki\MainConfigNames; |
| 15 | use MediaWiki\Title\NamespaceInfo; |
| 16 | use Wikimedia\Bcp47Code\Bcp47Code; |
| 17 | use 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 | */ |
| 26 | class 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 */ |
| 230 | class_alias( LanguageFactory::class, 'MediaWiki\\Languages\\LanguageFactory' ); |