assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); $this->options = $options; $this->localisationCache = $localisationCache; $this->langNameUtils = $langNameUtils; $this->langFallback = $langFallback; $this->langConverterFactory = $langConverterFactory; $this->hookContainer = $hookContainer; $this->langObjCache = new MapCacheLRU( self::LANG_CACHE_SIZE ); } /** * Get a cached or new language object for a given language code * @param string $code * @throws MWException if the language code contains dangerous characters, e.g. HTML special * characters or characters illegal in MediaWiki titles. * @return Language */ public function getLanguage( $code ): Language { $code = $this->options->get( 'DummyLanguageCodes' )[$code] ?? $code; $langObj = $this->langObjCache->get( $code ); if ( !$langObj ) { $langObj = $this->newFromCode( $code ); $this->langObjCache->set( $code, $langObj ); } return $langObj; } /** * Create a language object for a given language code * @param string $code * @param bool $fallback Whether we're going through language fallback chain * @throws MWException if the language code or fallback sequence is invalid * @return Language */ private function newFromCode( $code, $fallback = false ): Language { if ( !$this->langNameUtils->isValidCode( $code ) ) { throw new MWException( "Invalid language code \"$code\"" ); } $constructorArgs = [ $code, $this->localisationCache, $this->langNameUtils, $this->langFallback, $this->langConverterFactory, $this->hookContainer ]; if ( !$this->langNameUtils->isValidBuiltInCode( $code ) ) { // It's not possible to customise this code with class files, so // just return a Language object. This is to support uselang= hacks. return new Language( ...$constructorArgs ); } // Check if there is a language class for the code $class = $this->classFromCode( $code, $fallback ); // LanguageCode does not inherit Language if ( class_exists( $class ) && is_a( $class, 'Language', true ) ) { return new $class( ...$constructorArgs ); } // Keep trying the fallback list until we find an existing class $fallbacks = $this->langFallback->getAll( $code ); foreach ( $fallbacks as $fallbackCode ) { $class = $this->classFromCode( $fallbackCode ); if ( class_exists( $class ) ) { // TODO allow additional dependencies to be injected for subclasses somehow return new $class( ...$constructorArgs ); } } throw new MWException( "Invalid fallback sequence for language '$code'" ); } /** * @param string $code * @param bool $fallback Whether we're going through language fallback chain * @return string Name of the language class */ private function classFromCode( $code, $fallback = true ) { if ( $fallback && $code == 'en' ) { return 'Language'; } else { return 'Language' . str_replace( '-', '_', ucfirst( $code ) ); } } /** * Get the "parent" language which has a converter to convert a "compatible" language * (in another variant) to this language (eg. zh for zh-cn, but not en for en-gb). * * @param string $code * @return Language|null * @since 1.22 */ public function getParentLanguage( $code ) { // We deliberately use array_key_exists() instead of isset() because we cache null. if ( !array_key_exists( $code, $this->parentLangCache ) ) { $codeBase = explode( '-', $code )[0]; if ( !in_array( $codeBase, LanguageConverter::$languagesWithVariants ) ) { $this->parentLangCache[$code] = null; return null; } $lang = $this->getLanguage( $codeBase ); $converter = $this->langConverterFactory->getLanguageConverter( $lang ); if ( !$converter->hasVariant( $code ) ) { $this->parentLangCache[$code] = null; return null; } $this->parentLangCache[$code] = $lang; } return $this->parentLangCache[$code]; } }