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 MediaWiki\Config\ServiceOptions; 31use MediaWiki\HookContainer\HookContainer; 32use MediaWiki\MediaWikiServices; 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 array */ 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 'LangObjCacheSize', 73 ]; 74 75 /** 76 * @param ServiceOptions $options 77 * @param LocalisationCache $localisationCache 78 * @param LanguageNameUtils $langNameUtils 79 * @param LanguageFallback $langFallback 80 * @param LanguageConverterFactory $langConverterFactory 81 * @param HookContainer $hookContainer 82 */ 83 public function __construct( 84 ServiceOptions $options, 85 LocalisationCache $localisationCache, 86 LanguageNameUtils $langNameUtils, 87 LanguageFallback $langFallback, 88 LanguageConverterFactory $langConverterFactory, 89 HookContainer $hookContainer 90 ) { 91 $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); 92 93 $this->options = $options; 94 $this->localisationCache = $localisationCache; 95 $this->langNameUtils = $langNameUtils; 96 $this->langFallback = $langFallback; 97 $this->langConverterFactory = $langConverterFactory; 98 $this->hookContainer = $hookContainer; 99 } 100 101 /** 102 * Get a cached or new language object for a given language code 103 * @param string $code 104 * @throws MWException if the language code contains dangerous characters, e.g. HTML special 105 * characters or characters illegal in MediaWiki titles. 106 * @return Language 107 */ 108 public function getLanguage( $code ) : Language { 109 $code = $this->options->get( 'DummyLanguageCodes' )[$code] ?? $code; 110 111 // This is horrible, horrible code, but is necessary to support Language::$mLangObjCache 112 // per the deprecation policy. Kill with fire in 1.36! 113 if ( 114 MediaWikiServices::hasInstance() && 115 $this === MediaWikiServices::getInstance()->getLanguageFactory() 116 ) { 117 $this->langObjCache = Language::$mLangObjCache; 118 } 119 120 // Get the language object to process 121 $langObj = $this->langObjCache[$code] ?? $this->newFromCode( $code ); 122 123 // Merge the language object in to get it up front in the cache 124 $this->langObjCache = array_merge( [ $code => $langObj ], $this->langObjCache ); 125 // Get rid of the oldest ones in case we have an overflow 126 $this->langObjCache = 127 array_slice( $this->langObjCache, 0, $this->options->get( 'LangObjCacheSize' ), true ); 128 129 // As above, remove this in 1.36 130 if ( 131 MediaWikiServices::hasInstance() && 132 $this === MediaWikiServices::getInstance()->getLanguageFactory() 133 ) { 134 Language::$mLangObjCache = $this->langObjCache; 135 } 136 137 return $langObj; 138 } 139 140 /** 141 * Create a language object for a given language code 142 * @param string $code 143 * @param bool $fallback Whether we're going through language fallback chain 144 * @throws MWException if the language code or fallback sequence is invalid 145 * @return Language 146 */ 147 private function newFromCode( $code, $fallback = false ) : Language { 148 if ( !$this->langNameUtils->isValidCode( $code ) ) { 149 throw new MWException( "Invalid language code \"$code\"" ); 150 } 151 152 $constructorArgs = [ 153 $code, 154 $this->localisationCache, 155 $this->langNameUtils, 156 $this->langFallback, 157 $this->langConverterFactory, 158 $this->hookContainer 159 ]; 160 161 if ( !$this->langNameUtils->isValidBuiltInCode( $code ) ) { 162 // It's not possible to customise this code with class files, so 163 // just return a Language object. This is to support uselang= hacks. 164 return new Language( ...$constructorArgs ); 165 } 166 167 // Check if there is a language class for the code 168 $class = $this->classFromCode( $code, $fallback ); 169 // LanguageCode does not inherit Language 170 if ( class_exists( $class ) && is_a( $class, 'Language', true ) ) { 171 return new $class( ...$constructorArgs ); 172 } 173 174 // Keep trying the fallback list until we find an existing class 175 $fallbacks = $this->langFallback->getAll( $code ); 176 foreach ( $fallbacks as $fallbackCode ) { 177 $class = $this->classFromCode( $fallbackCode ); 178 if ( class_exists( $class ) ) { 179 // TODO allow additional dependencies to be injected for subclasses somehow 180 return new $class( ...$constructorArgs ); 181 } 182 } 183 184 throw new MWException( "Invalid fallback sequence for language '$code'" ); 185 } 186 187 /** 188 * @param string $code 189 * @param bool $fallback Whether we're going through language fallback chain 190 * @return string Name of the language class 191 */ 192 private function classFromCode( $code, $fallback = true ) { 193 if ( $fallback && $code == 'en' ) { 194 return 'Language'; 195 } else { 196 return 'Language' . str_replace( '-', '_', ucfirst( $code ) ); 197 } 198 } 199 200 /** 201 * Get the "parent" language which has a converter to convert a "compatible" language 202 * (in another variant) to this language (eg. zh for zh-cn, but not en for en-gb). 203 * 204 * @param string $code 205 * @return Language|null 206 * @since 1.22 207 */ 208 public function getParentLanguage( $code ) { 209 // We deliberately use array_key_exists() instead of isset() because we cache null. 210 if ( !array_key_exists( $code, $this->parentLangCache ) ) { 211 $codeBase = explode( '-', $code )[0]; 212 if ( !in_array( $codeBase, LanguageConverter::$languagesWithVariants ) ) { 213 $this->parentLangCache[$code] = null; 214 return null; 215 } 216 217 $lang = $this->getLanguage( $codeBase ); 218 $converter = $this->langConverterFactory->getLanguageConverter( $lang ); 219 if ( !$converter->hasVariant( $code ) ) { 220 $this->parentLangCache[$code] = null; 221 return null; 222 } 223 224 $this->parentLangCache[$code] = $lang; 225 } 226 227 return $this->parentLangCache[$code]; 228 } 229} 230