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 21/** 22 * @defgroup Language Language 23 */ 24 25namespace MediaWiki\Languages; 26 27use Language; 28use LanguageConverter; 29use LocalisationCache; 30use MapCacheLRU; 31use MediaWiki\Config\ServiceOptions; 32use MediaWiki\HookContainer\HookContainer; 33use MWException; 34 35/** 36 * Internationalisation code 37 * See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more information. 38 * 39 * @ingroup Language 40 * @since 1.35 41 */ 42class LanguageFactory { 43 /** @var ServiceOptions */ 44 private $options; 45 46 /** @var LocalisationCache */ 47 private $localisationCache; 48 49 /** @var LanguageNameUtils */ 50 private $langNameUtils; 51 52 /** @var LanguageFallback */ 53 private $langFallback; 54 55 /** @var LanguageConverterFactory */ 56 private $langConverterFactory; 57 58 /** @var HookContainer */ 59 private $hookContainer; 60 61 /** @var MapCacheLRU */ 62 private $langObjCache; 63 64 /** @var array */ 65 private $parentLangCache = []; 66 67 /** 68 * @internal For use by ServiceWiring 69 */ 70 public const CONSTRUCTOR_OPTIONS = [ 71 'DummyLanguageCodes', 72 ]; 73 74 /** How many distinct Language objects to retain at most in memory (T40439). */ 75 private const LANG_CACHE_SIZE = 10; 76 77 /** 78 * @param ServiceOptions $options 79 * @param LocalisationCache $localisationCache 80 * @param LanguageNameUtils $langNameUtils 81 * @param LanguageFallback $langFallback 82 * @param LanguageConverterFactory $langConverterFactory 83 * @param HookContainer $hookContainer 84 */ 85 public function __construct( 86 ServiceOptions $options, 87 LocalisationCache $localisationCache, 88 LanguageNameUtils $langNameUtils, 89 LanguageFallback $langFallback, 90 LanguageConverterFactory $langConverterFactory, 91 HookContainer $hookContainer 92 ) { 93 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); 94 95 $this->options = $options; 96 $this->localisationCache = $localisationCache; 97 $this->langNameUtils = $langNameUtils; 98 $this->langFallback = $langFallback; 99 $this->langConverterFactory = $langConverterFactory; 100 $this->hookContainer = $hookContainer; 101 $this->langObjCache = new MapCacheLRU( self::LANG_CACHE_SIZE ); 102 } 103 104 /** 105 * Get a cached or new language object for a given language code 106 * @param string $code 107 * @throws MWException if the language code contains dangerous characters, e.g. HTML special 108 * characters or characters illegal in MediaWiki titles. 109 * @return Language 110 */ 111 public function getLanguage( $code ): Language { 112 $code = $this->options->get( 'DummyLanguageCodes' )[$code] ?? $code; 113 $langObj = $this->langObjCache->get( $code ); 114 115 if ( !$langObj ) { 116 $langObj = $this->newFromCode( $code ); 117 $this->langObjCache->set( $code, $langObj ); 118 } 119 120 return $langObj; 121 } 122 123 /** 124 * Create a language object for a given language code 125 * @param string $code 126 * @param bool $fallback Whether we're going through language fallback chain 127 * @throws MWException if the language code or fallback sequence is invalid 128 * @return Language 129 */ 130 private function newFromCode( $code, $fallback = false ): Language { 131 if ( !$this->langNameUtils->isValidCode( $code ) ) { 132 throw new MWException( "Invalid language code \"$code\"" ); 133 } 134 135 $constructorArgs = [ 136 $code, 137 $this->localisationCache, 138 $this->langNameUtils, 139 $this->langFallback, 140 $this->langConverterFactory, 141 $this->hookContainer 142 ]; 143 144 if ( !$this->langNameUtils->isValidBuiltInCode( $code ) ) { 145 // It's not possible to customise this code with class files, so 146 // just return a Language object. This is to support uselang= hacks. 147 return new Language( ...$constructorArgs ); 148 } 149 150 // Check if there is a language class for the code 151 $class = $this->classFromCode( $code, $fallback ); 152 // LanguageCode does not inherit Language 153 if ( class_exists( $class ) && is_a( $class, 'Language', true ) ) { 154 return new $class( ...$constructorArgs ); 155 } 156 157 // Keep trying the fallback list until we find an existing class 158 $fallbacks = $this->langFallback->getAll( $code ); 159 foreach ( $fallbacks as $fallbackCode ) { 160 $class = $this->classFromCode( $fallbackCode ); 161 if ( class_exists( $class ) ) { 162 // TODO allow additional dependencies to be injected for subclasses somehow 163 return new $class( ...$constructorArgs ); 164 } 165 } 166 167 throw new MWException( "Invalid fallback sequence for language '$code'" ); 168 } 169 170 /** 171 * @param string $code 172 * @param bool $fallback Whether we're going through language fallback chain 173 * @return string Name of the language class 174 */ 175 private function classFromCode( $code, $fallback = true ) { 176 if ( $fallback && $code == 'en' ) { 177 return 'Language'; 178 } else { 179 return 'Language' . str_replace( '-', '_', ucfirst( $code ) ); 180 } 181 } 182 183 /** 184 * Get the "parent" language which has a converter to convert a "compatible" language 185 * (in another variant) to this language (eg. zh for zh-cn, but not en for en-gb). 186 * 187 * @param string $code 188 * @return Language|null 189 * @since 1.22 190 */ 191 public function getParentLanguage( $code ) { 192 // We deliberately use array_key_exists() instead of isset() because we cache null. 193 if ( !array_key_exists( $code, $this->parentLangCache ) ) { 194 $codeBase = explode( '-', $code )[0]; 195 if ( !in_array( $codeBase, LanguageConverter::$languagesWithVariants ) ) { 196 $this->parentLangCache[$code] = null; 197 return null; 198 } 199 200 $lang = $this->getLanguage( $codeBase ); 201 $converter = $this->langConverterFactory->getLanguageConverter( $lang ); 202 if ( !$converter->hasVariant( $code ) ) { 203 $this->parentLangCache[$code] = null; 204 return null; 205 } 206 207 $this->parentLangCache[$code] = $lang; 208 } 209 210 return $this->parentLangCache[$code]; 211 } 212} 213