From: Matthew Walker Date: Wed, 16 Jan 2013 07:28:54 +0000 (-0800) Subject: (bug 1495) Enable on-wiki message language fallbacks X-Git-Tag: 1.31.0-rc.0~20440^2 X-Git-Url: https://git.cyclocoop.org//%22?a=commitdiff_plain;h=d434bfcf3bbab05660ed8f798a4622487dd8ba56;p=lhc%2Fweb%2Fwiklou.git (bug 1495) Enable on-wiki message language fallbacks The core function behind wfMessage() (MessageCache->get()) did not apply the language fallback chain to on-wiki messages. This patch has changed the behavior to iterate over all possible on-wiki fallbacks (starting with the user's language) before using the built-in language cache (CDB files). Previously we only looked for the existence of an on-wiki message in the users's language. Performance wise, using the 'ab' language ('ru', 'en' fallbacks) MessageCache::get (Averaged over runs and calls) New Code: ~8.5% TET (110us/call) Old Code: ~6.5% TET ( 90us/call) TET: Total Execution Time Change-Id: Iaaf6ccebd8c40c9602748c58c3a5c73c29e7aa4d --- diff --git a/includes/cache/MessageCache.php b/includes/cache/MessageCache.php index 89e5325028..f24e8bcde4 100644 --- a/includes/cache/MessageCache.php +++ b/includes/cache/MessageCache.php @@ -578,50 +578,70 @@ class MessageCache { } /** - * Get a message from either the content language or the user language. + * Get a message from either the content language or the user language. The fallback + * language order is the users language fallback union the content language fallback. + * This list is then applied to find keys in the following order + * 1) MediaWiki:$key/$langcode (for every language except the content language where + * we look at MediaWiki:$key) + * 2) Built-in messages via the l10n cache which is also in fallback order * * @param $key String: the message cache key - * @param $useDB Boolean: get the message from the DB, false to use only - * the localisation - * @param bool|string $langcode Code of the language to get the message for, if - * it is a valid code create a language for that language, - * if it is a string but not a valid code then make a basic - * language object, if it is a false boolean then use the - * current users language (as a fallback for the old - * parameter functionality), or if it is a true boolean - * then use the wikis content language (also as a - * fallback). + * @param $useDB Boolean: If true will look for the message in the DB, false only + * get the message from the DB, false to use only the compiled l10n cache. + * @param bool|string|object $langcode Code of the language to get the message for. + * - If string and a valid code, will create a standard language object + * - If string but not a valid code, will create a basic language object + * - If boolean and false, create object from the current users language + * - If boolean and true, create object from the wikis content language + * - If language object, use it as given * @param $isFullKey Boolean: specifies whether $key is a two part key * "msg/lang". * * @throws MWException - * @return string|bool + * @return string|bool False if the message doesn't exist, otherwise the message */ function get( $key, $useDB = true, $langcode = true, $isFullKey = false ) { global $wgLanguageCode, $wgContLang; + wfProfileIn( __METHOD__ ); + if ( is_int( $key ) ) { // "Non-string key given" exception sometimes happens for numerical strings that become ints somewhere on their way here $key = strval( $key ); } if ( !is_string( $key ) ) { + wfProfileOut( __METHOD__ ); throw new MWException( 'Non-string key given' ); } if ( strval( $key ) === '' ) { # Shortcut: the empty key is always missing + wfProfileOut( __METHOD__ ); return false; } - $lang = wfGetLangObj( $langcode ); - if ( !$lang ) { - throw new MWException( "Bad lang code $langcode given" ); - } - $langcode = $lang->getCode(); + # Obtain the initial language object + if ( $isFullKey ) { + $keyParts = explode( '/', $key ); + if ( count( $keyParts ) < 2 ) { + throw new MWException( "Message key '$key' does not appear to be a full key." ); + } - $message = false; + $langcode = array_pop( $keyParts ); + $key = implode( '/', $keyParts ); + } + + # Obtain a language object for the requested language from the passed language code + # Note that the language code could in fact be a language object already but we assume + # it's a string further below. + $requestedLangObj = wfGetLangObj( $langcode ); + if ( !$requestedLangObj ) { + wfProfileOut( __METHOD__ ); + throw new MWException( "Bad lang code $langcode given" ); + } + $langcode = $requestedLangObj->getCode(); # Normalise title-case input (with some inlining) $lckey = str_replace( ' ', '_', $key ); @@ -633,24 +653,37 @@ class MessageCache { $uckey = $wgContLang->ucfirst( $lckey ); } + # Loop through each language in the fallback list until we find something useful + $message = false; + # Try the MediaWiki namespace - if( !$this->mDisable && $useDB ) { - $title = $uckey; - if( !$isFullKey && ( $langcode != $wgLanguageCode ) ) { - $title .= '/' . $langcode; + if ( !$this->mDisable && $useDB ) { + $fallbackChain = Language::getFallbacksIncludingSiteLanguage( $langcode ); + array_unshift( $fallbackChain, $langcode ); + + foreach ( $fallbackChain as $langcode ) { + if ( $langcode === $wgLanguageCode ) { + # Messages created in the content language will not have the /lang extension + $message = $this->getMsgFromNamespace( $uckey, $langcode ); + } else { + $message = $this->getMsgFromNamespace( "$uckey/$langcode", $langcode ); + } + + if ( $message !== false ) { + break; + } } - $message = $this->getMsgFromNamespace( $title, $langcode ); } # Try the array in the language object if ( $message === false ) { - $message = $lang->getMessage( $lckey ); - if ( is_null( $message ) ) { + $message = $requestedLangObj->getMessage( $lckey ); + if ( is_null ( $message ) ) { $message = false; } } - # Try the array of another language + # If we still have no message, maybe the key was in fact a full key so try that if( $message === false ) { $parts = explode( '/', $lckey ); # We may get calls for things that are http-urls from sidebar @@ -664,15 +697,9 @@ class MessageCache { } } - # Is this a custom message? Try the default language in the db... - if( ( $message === false || $message === '-' ) && - !$this->mDisable && $useDB && - !$isFullKey && ( $langcode != $wgLanguageCode ) ) { - $message = $this->getMsgFromNamespace( $uckey, $wgLanguageCode ); - } - # Final fallback if( $message === false ) { + wfProfileOut( __METHOD__ ); return false; } @@ -686,6 +713,7 @@ class MessageCache { ' ' => "\xc2\xa0", ) ); + wfProfileOut( __METHOD__ ); return $message; } diff --git a/languages/Language.php b/languages/Language.php index ef6a367b88..01751db7ed 100644 --- a/languages/Language.php +++ b/languages/Language.php @@ -3984,6 +3984,45 @@ class Language { } } + /** + * Get the ordered list of fallback languages, ending with the fallback + * language chain for the site language. + * + * @since 1.21 + * @param $code string Language code + * @return array + */ + public static function getFallbacksIncludingSiteLanguage( $code ) { + global $wgLanguageCode; + + // Usually, we will only store a tiny number of fallback chains, so we + // keep them in static memory. + static $fallbackLanguageCache = array(); + $cacheKey = "{$code}-{$wgLanguageCode}"; + + if ( !array_key_exists( $cacheKey, $fallbackLanguageCache ) ) { + $fallbacks = self::getFallbacksFor( $code ); + + // Take the final 'en' off of the array before splicing + if ( end( $fallbacks ) === 'en' ) { + array_pop( $fallbacks ); + } + // Append the site's fallback chain + $siteFallbacks = self::getFallbacksFor( $wgLanguageCode ); + + // Eliminate any languages already included in the chain + $siteFallbacks = array_intersect( array_diff( $siteFallbacks, $fallbacks ), $siteFallbacks ); + if ( $siteFallbacks ) { + $fallbacks = array_merge( $fallbacks, $siteFallbacks ); + } + if ( end( $fallbacks ) !== 'en' ) { + $fallbacks[] = 'en'; + } + $fallbackLanguageCache[$cacheKey] = $fallbacks; + } + return $fallbackLanguageCache[$cacheKey]; + } + /** * Get all messages for a given language * WARNING: this may take a long time. If you just need all message *keys* diff --git a/tests/phpunit/includes/cache/MessageCacheTest.php b/tests/phpunit/includes/cache/MessageCacheTest.php new file mode 100644 index 0000000000..89021ddfac --- /dev/null +++ b/tests/phpunit/includes/cache/MessageCacheTest.php @@ -0,0 +1,88 @@ +enable(); + } + + function addDBData() { + // Set up messages and fallbacks ab -> ru -> en + $this->makePage( 'FallbackLanguageTest-Full', 'ab' ); + $this->makePage( 'FallbackLanguageTest-Full', 'ru' ); + $this->makePage( 'FallbackLanguageTest-Full', 'en' ); + + // Fallbacks where ab does not exist + $this->makePage( 'FallbackLanguageTest-Partial', 'ru' ); + $this->makePage( 'FallbackLanguageTest-Partial', 'en' ); + + // Fallback to english + $this->makePage( 'FallbackLanguageTest-English', 'en' ); + + // Full key tests -- always want russian + $this->makePage( 'MessageCacheTest-FullKeyTest', 'ab' ); + $this->makePage( 'MessageCacheTest-FullKeyTest', 'ru' ); + } + + /** + * Helper function for addDBData -- adds a simple page to the database + * + * @param string $title Title of page to be created + * @param string $lang Language and content of the created page + */ + protected function makePage( $title, $lang ) { + global $wgContLang; + + $title = Title::newFromText( + ($lang == $wgContLang->getCode()) ? $title : "$title/$lang", + NS_MEDIAWIKI + ); + $wikiPage = new WikiPage( $title ); + $content = ContentHandler::makeContent( $lang, $title ); + $wikiPage->doEditContent( $content, "$lang translation test case" ); + } + + /** + * Test message fallbacks, bug #1495 + * + * @dataProvider provideMessagesForFallback + */ + function testMessageFallbacks( $message, $lang, $expectedContent ) { + $result = MessageCache::singleton()->get( $message, true, $lang ); + $this->assertEquals( $expectedContent, $result, "Message fallback failed." ); + } + + function provideMessagesForFallback() { + return array( + array( 'FallbackLanguageTest-Full', 'ab', 'ab' ), + array( 'FallbackLanguageTest-Partial', 'ab', 'ru' ), + array( 'FallbackLanguageTest-English', 'ab', 'en' ), + array( 'FallbackLanguageTest-None', 'ab', false ), + ); + } + + /** + * There's a fallback case where the message key is given as fully qualified -- this + * should ignore the passed $lang and use the language from the key + * + * @dataProvider provideMessagesForFullKeys + */ + function testFullKeyBehaviour( $message, $lang, $expectedContent ) { + $result = MessageCache::singleton()->get( $message, true, $lang, true ); + $this->assertEquals( $expectedContent, $result, "Full key message fallback failed." ); + } + + function provideMessagesForFullKeys() { + return array( + array( 'MessageCacheTest-FullKeyTest/ru', 'ru', 'ru' ), + array( 'MessageCacheTest-FullKeyTest/ru', 'ab', 'ru' ), + array( 'MessageCacheTest-FullKeyTest/ru/foo', 'ru', false ), + ); + } + +}