diff --git a/Scribunto.php b/Scribunto.php index 7adfc1df..b6feb82f 100644 --- a/Scribunto.php +++ b/Scribunto.php @@ -103,6 +103,7 @@ $wgAutoloadClasses['Scribunto_LuaDataProvider'] = $dir.'tests/engines/LuaCommon/ $wgAutoloadClasses['Scribunto_LuaSiteLibrary'] = $dir.'engines/LuaCommon/SiteLibrary.php'; $wgAutoloadClasses['Scribunto_LuaUriLibrary'] = $dir.'engines/LuaCommon/UriLibrary.php'; $wgAutoloadClasses['Scribunto_LuaUstringLibrary'] = $dir.'engines/LuaCommon/UstringLibrary.php'; +$wgAutoloadClasses['Scribunto_LuaLanguageLibrary'] = $dir.'engines/LuaCommon/LanguageLibrary.php'; /***** Configuration *****/ diff --git a/engines/LuaCommon/LanguageLibrary.php b/engines/LuaCommon/LanguageLibrary.php new file mode 100644 index 00000000..54a94c72 --- /dev/null +++ b/engines/LuaCommon/LanguageLibrary.php @@ -0,0 +1,269 @@ +langCache[$wgContLang->getCode()] = $wgContLang; + + $statics = array( + 'getContLangCode', + 'isSupportedLanguage', + 'isKnownLanguageTag', + 'isValidCode', + 'isValidBuiltInCode', + 'fetchLanguageName', + ); + $methods = array( + 'lcfirst', + 'ucfirst', + 'lc', + 'uc', + 'caseFold', + 'formatNum', + 'formatDate', + 'parseFormattedNumber', + 'convertPlural', + 'convertGrammar', + 'gender', + 'isRTL', + ); + $lib = array(); + foreach ( $statics as $name ) { + $lib[$name] = array( $this, $name ); + } + $ths = $this; + foreach ( $methods as $name ) { + $lib[$name] = function () use ( $ths, $name ) { + $args = func_get_args(); + return $ths->languageMethod( $name, $args ); + }; + } + $this->getEngine()->registerInterface( 'mw.language.lua', $lib ); + } + + function getContLangCode() { + global $wgContLang; + return array( $wgContLang->getCode() ); + } + + function isSupportedLanguage( $code ) { + $this->checkType( 'isSupportedLanguage', 1, $code, 'string' ); + return array( Language::isSupportedLanguage( $code ) ); + } + + function isKnownLanguageTag( $code ) { + $this->checkType( 'isKnownLanguageTag', 1, $code, 'string' ); + return array( Language::isKnownLanguageTag( $code ) ); + } + + function isValidCode( $code ) { + $this->checkType( 'isValidCode', 1, $code, 'string' ); + return array( Language::isValidCode( $code ) ); + } + + function isValidBuiltInCode( $code ) { + $this->checkType( 'isValidBuiltInCode', 1, $code, 'string' ); + return array( (bool)Language::isValidBuiltInCode( $code ) ); + } + + function fetchLanguageName( $code, $inLanguage ) { + $this->checkType( 'fetchLanguageName', 1, $code, 'string' ); + $this->checkTypeOptional( 'fetchLanguageName', 2, $inLanguage, 'string', null ); + return array( Language::fetchLanguageName( $code, $inLanguage ) ); + } + + /** + * Language object method handler + */ + function languageMethod( $name, $args ) { + $name = strval( $name ); + $code = array_shift( $args ); + if ( !isset( $this->langCache[$code] ) ) { + if ( count( $this->langCache ) > self::MAX_LANG_CACHE_SIZE ) { + throw new Scribunto_LuaError( 'too many language codes requested' ); + } + $this->langCache[$code] = Language::factory( $code ); + } + $lang = $this->langCache[$code]; + switch ( $name ) { + // Zero arguments + case 'isRTL': + return array( $lang->$name() ); + + // One string argument passed straight through + case 'lcfirst': + case 'ucfirst': + case 'lc': + case 'uc': + case 'caseFold': + $this->checkType( $name, 1, $args[0], 'string' ); + return array( $lang->$name( $args[0] ) ); + + case 'parseFormattedNumber': + if ( is_numeric( $args[0] ) ) { + $args[0] = strval( $args[0] ); + } + $this->checkType( $name, 1, $args[0], 'string' ); + return array( $lang->$name( $args[0] ) ); + + // Custom handling + default: + return $this->$name( $lang, $args ); + } + } + + /** + * convertPlural handler + */ + function convertPlural( $lang, $args ) { + $number = array_shift( $args ); + $this->checkType( 'convertPlural', 1, $number, 'number' ); + if ( is_array( $args[0] ) ) { + $args = $args[0]; + } + $forms = array_values( array_map( 'strval', $args ) ); + return array( $lang->convertPlural( $number, $forms ) ); + } + + /** + * convertGrammar handler + */ + function convertGrammar( $lang, $args ) { + $this->checkType( 'convertGrammar', 1, $args[0], 'string' ); + $this->checkType( 'convertGrammar', 2, $args[1], 'string' ); + return array( $lang->convertGrammar( $args[0], $args[1] ) ); + } + + /** + * gender handler + */ + function gender( $lang, $args ) { + $this->checkType( 'gender', 1, $args[0], 'string' ); + $username = trim( array_shift( $args[0] ) ); + + if ( is_array( $args[0] ) ) { + $args = $args[0]; + } + $forms = array_values( array_map( 'strval', $args ) ); + + // Shortcuts + if ( count( $forms ) === 0 ) { + return ''; + } elseif ( count( $forms ) === 1 ) { + return $forms[0]; + } + + if ( $username === 'male' || $username === 'female' ) { + $gender = $username; + } else { + // default + $gender = User::getDefaultOption( 'gender' ); + + // Check for "User:" prefix + $title = Title::newFromText( $username ); + if ( $title && $title->getNamespace() == NS_USER ) { + $username = $title->getText(); + } + + // check parameter, or use the ParserOptions if in interface message + $user = User::newFromName( $username ); + if ( $user ) { + $gender = GenderCache::singleton()->getGenderOf( $user, __METHOD__ ); + } elseif ( $username === '' ) { + $parserOptions = $this->getParserOptions(); + if ( $parserOptions->getInterfaceMessage() ) { + $gender = GenderCache::singleton()->getGenderOf( $parserOptions->getUser(), __METHOD__ ); + } + } + } + return array( $lang->gender( $gender, $forms ) ); + } + + /** + * formatNum handler + */ + function formatNum( $lang, $args ) { + $num = $args[0]; + $this->checkType( 'formatNum', 1, $num, 'number' ); + + $noCommafy = false; + if ( isset( $args[1] ) ) { + $this->checkType( 'formatNum', 2, $args[1], 'table' ); + $options = $args[1]; + $noCommafy = !empty( $options['noCommafy'] ); + } + return array( $lang->formatNum( $num, $noCommafy ) ); + } + + /** + * formatDate handler + */ + function formatDate( $lang, $args ) { + $this->checkType( 'formatDate', 1, $args[0], 'string' ); + $this->checkTypeOptional( 'formatDate', 2, $args[1], 'string', '' ); + $this->checkTypeOptional( 'formatDate', 3, $args[2], 'boolean', false ); + + list( $format, $date, $local ) = $args; + $langcode = $lang->getCode(); + + if ( $date === '' ) { + $cacheKey = $this->getParserOptions()->getTimestamp(); + $timestamp = new MWTimestamp( $cacheKey ); + $date = $timestamp->getTimestamp( TS_ISO_8601 ); + } else { + # Correct for DateTime interpreting 'XXXX' as XX:XX o'clock + if ( preg_match( '/^[0-9]{4}$/', $date ) ) { + $date = '00:00 '.$date; + } + + $cacheKey = $date; + } + + if ( isset( $this->timeCache[$format][$cacheKey][$langcode][$local] ) ) { + return array( $this->timeCache[$format][$cacheKey][$langcode][$local] ); + } + + $this->timeChars += strlen( $format ); + if ( $this->timeChars > self::MAX_TIME_CHARS ) { + throw new Scribunto_LuaError( "Too many calls to mw.language:formatDate()" ); + } + + # Default input timezone is UTC. + try { + $utc = new DateTimeZone( 'UTC' ); + $dateObject = new DateTime( $date, $utc ); + } catch ( Exception $ex ) { + throw new Scribunto_LuaError( "bad argument #2 to 'formatDate' (not a valid timestamp)" ); + } + + # Set output timezone. + if ( $local ) { + if ( isset( $wgLocaltimezone ) ) { + $tz = new DateTimeZone( $wgLocaltimezone ); + } else { + $tz = new DateTimeZone( date_default_timezone_get() ); + } + $dateObject->setTimezone( $tz ); + } else { + $dateObject->setTimezone( $utc ); + } + # Generate timestamp + $ts = $dateObject->format( 'YmdHis' ); + + if ( $ts >= 100000000000000 ) { + throw new Scribunto_LuaError( "mw.language:formatDate() only supports years up to 9999" ); + } + + $ret = $lang->sprintfDate( $format, $ts ); + $this->timeCache[$format][$cacheKey][$langcode][$local] = $ret; + return array( $ret ); + } +} diff --git a/engines/LuaCommon/LuaCommon.php b/engines/LuaCommon/LuaCommon.php index 284fd71a..5db2edd6 100644 --- a/engines/LuaCommon/LuaCommon.php +++ b/engines/LuaCommon/LuaCommon.php @@ -9,6 +9,7 @@ abstract class Scribunto_LuaEngine extends ScribuntoEngineBase { 'mw.site' => 'Scribunto_LuaSiteLibrary', 'mw.uri' => 'Scribunto_LuaUriLibrary', 'mw.ustring' => 'Scribunto_LuaUstringLibrary', + 'mw.language' => 'Scribunto_LuaLanguageLibrary', ); /** diff --git a/engines/LuaCommon/lualib/mw.language.lua b/engines/LuaCommon/lualib/mw.language.lua new file mode 100644 index 00000000..da76aafe --- /dev/null +++ b/engines/LuaCommon/lualib/mw.language.lua @@ -0,0 +1,114 @@ +local language = {} +local php +local util = require 'libraryUtil' + +function language.setupInterface() + -- Boilerplate + language.setupInterface = nil + php = mw_interface + mw_interface = nil + + -- Register this library in the "mw" global + mw = mw or {} + mw.language = language + mw.getContentLanguage = language.getContentLanguage + mw.getLanguage = mw.language.new + + local lang = mw.getContentLanguage(); + + -- Extend ustring + if mw.ustring then + mw.ustring.upper = function ( s ) + return lang:uc( s ) + end + mw.ustring.lower = function ( s ) + return lang:lc( s ) + end + string.uupper = mw.ustring.upper + string.ulower = mw.ustring.lower + end + + package.loaded['mw.language'] = language +end + +function language.isSupportedLanguage( code ) + return php.isSupportedLanguage( code ) +end + +function language.isKnownLanguageTag( code ) + return php.isKnownLanguageTag( code ) +end + +function language.isValidCode( code ) + return php.isValidCode( code ) +end + +function language.isValidBuiltInCode( code ) + return php.isValidBuiltInCode( code ) +end + +function language.fetchLanguageName( code, inLanguage ) + return php.fetchLanguageName( code, inLanguage ) +end + +function language.new( code ) + if code == nil then + error( "too few arguments to mw.language.new()", 2 ) + end + + local lang = { code = code } + + local checkSelf = util.makeCheckSelfFunction( 'mw.language', 'lang', lang, 'language object' ) + + local wrappers = { + isRTL = 0, + lcfirst = 1, + ucfirst = 1, + lc = 1, + uc = 1, + caseFold = 1, + formatNum = 1, + formatDate = 1, + parseFormattedNumber = 1, + convertPlural = 2, + convertGrammar = 2, + gender = 2, + } + + for name, numArgs in pairs( wrappers ) do + lang[name] = function ( self, ... ) + checkSelf( self, name ) + if select( '#', ... ) < numArgs then + error( "too few arguments to mw.language:" .. name, 2 ) + end + return php[name]( self.code, ... ) + end + end + + -- Alias + lang.plural = lang.convertPlural + + -- Parser function compat + function lang:grammar( case, word ) + checkSelf( self, name ) + return self:convertGrammar( word, case ) + end + + function lang:getCode() + checkSelf( self, 'getCode' ) + return self.code + end + + return lang +end + +local contLangCode + +function language.getContentLanguage() + if contLangCode == nil then + contLangCode = php.getContLangCode() + end + return language.new( contLangCode ) +end + +return language