پودمان:Multilingual: تفاوت میان نسخه‌ها

    از ویکی‌نور
    fa>Wikinoor.ir
    جز (Protected "Module:Multilingual": High-risk Lua module: Requested by Pppery at RfPP. Used in Module:Format TemplateData, which is semi-protected and has 516 transclusions ([Edit=Require autoconfirmed or confirmed access] (indefinite)))
    (بدون تفاوت)

    نسخهٔ ‏۹ اوت ۲۰۲۲، ساعت ۰۳:۳۷

    توضیحات این پودمان می‌تواند در پودمان:Multilingual/توضیحات قرار گیرد.

    local Multilingual = { suite   = "Multilingual",
                           serial  = "2020-12-10",
                           item    = 47541920,
                           globals = { ISO15924 = 71584769,
                                       WLink    = 19363224 }
                         }
    --[=[
    Utilities for multilingual texts and ISO 639 (BCP47) issues etc.
    * fair()
    * fallback()
    * findCode()
    * fix()
    * format()
    * getBase()
    * getLang()
    * getName()
    * i18n()
    * int()
    * isLang()
    * isLangWiki()
    * isMinusculable()
    * isRTL()
    * message()
    * sitelink()
    * tabData()
    * userLang()
    * userLangCode()
    * wikibase()
    * failsafe()
    loadData: Multilingual/config Multilingual/names
    ]=]
    local Failsafe   = Multilingual
    local GlobalMod  = Multilingual
    local GlobalData = Multilingual
    local User       = { sniffer = "showpreview" }
    Multilingual.globals.Multilingual = Multilingual.item
    
    
    
    Multilingual.exotic = { simple = true,
                            no     = true }
    Multilingual.prefer = { cs = true,
                            de = true,
                            en = true,
                            es = true,
                            fr = true,
                            it = true,
                            nl = true,
                            pt = true,
                            ru = true,
                            sv = true }
    
    
    
    local foreignModule = function ( access, advanced, append, alt, alert )
        -- Fetch global module
        -- Precondition:
        --     access    -- string, with name of base module
        --     advanced  -- true, for require(); else mw.loadData()
        --     append    -- string, with subpage part, if any; or false
        --     alt       -- number, of wikidata item of root; or false
        --     alert     -- true, for throwing error on data problem
        -- Postcondition:
        --     Returns whatever, probably table
        -- 2020-01-01
        local storage = access
        local finer = function ()
                          if append then
                              storage = string.format( "%s/%s",
                                                       storage,
                                                       append )
                          end
                      end
        local fun, lucky, r, suited
        if advanced then
            fun = require
        else
            fun = mw.loadData
        end
        GlobalMod.globalModules = GlobalMod.globalModules or { }
        suited = GlobalMod.globalModules[ access ]
        if not suited then
            finer()
            lucky, r = pcall( fun,  "Module:" .. storage )
        end
        if not lucky then
            if not suited  and
               type( alt ) == "number"  and
               alt > 0 then
                suited = string.format( "Q%d", alt )
                suited = mw.wikibase.getSitelink( suited )
                GlobalMod.globalModules[ access ] = suited or true
            end
            if type( suited ) == "string" then
                storage = suited
                finer()
                lucky, r = pcall( fun, storage )
            end
            if not lucky and alert then
                error( "Missing or invalid page: " .. storage )
            end
        end
        return r
    end -- foreignModule()
    
    
    
    local fetchData = function ( access )
        -- Retrieve translated keyword from commons:Data:****.tab
        -- Precondition:
        --     access  -- string, with page identification on Commons
        --     Returns table, with data, or string, with error message
        -- 2019-12-05
        local storage = access
        local r
        if type( storage ) == "string" then
            local s
            storage = mw.text.trim( storage )
            s = storage:lower()
            if s:sub( 1, 2 ) == "c:" then
                storage = mw.text.trim( storage:sub( 3 ) )
                s       = storage:lower()
            elseif s:sub( 1, 8 ) == "commons:" then
                storage = mw.text.trim( storage:sub( 9 ) )
                s       = storage:lower()
            end
            if s:sub( 1, 5 ) == "data:" then
                storage = mw.text.trim( storage:sub( 6 ) )
                s       = storage:lower()
            end
            if s == ""  or  s == ".tab" then
                storage = false
            elseif s:sub( -4 ) == ".tab" then
                storage = storage:sub( 1, -5 ) .. ".tab"
            else
                storage = storage .. ".tab"
            end
        end
        if type( storage ) == "string" then
            local data
            if type( GlobalData.TabDATA ) ~= "table" then
                GlobalData.TabDATA = { }
            end
            data = GlobalData.TabDATA[ storage ]
            if data then
                r = data
            else
                local lucky
                lucky, data = pcall( mw.ext.data.get, storage, "_" )
                if type( data ) == "table" then
                    data = data.data
                    if type( data ) == "table" then
                        GlobalData.TabDATA[ storage ] = data
                    else
                        r = string.format( "%s [[%s%s]]",
                                           "INVALID Data:*.tab",
                                           "commons:Data:",
                                           storage )
                    end
                else
                    r = "BAD PAGE Data:*.tab – commons:" .. storage
                end
                if r then
                    GlobalData.TabDATA[ storage ] = r
                    data = false
                else
                    r = data
                end
            end
        else
            r = "BAD PAGE commons:Data:*.tab"
        end
        return r
    end -- fetchData()
    
    
    
    local favorites = function ()
        -- Provide fallback codes
        -- Postcondition:
        --     Returns table with sequence of preferred languages
        --     * ahead elements
        --     * user (not yet accessible)
        --     * page content language (not yet accessible)
        --     * page name subpage
        --     * project
        --     * en
        local r = Multilingual.polyglott
        if not r then
            local self = mw.language.getContentLanguage():getCode():lower()
            local sub  = mw.title.getCurrentTitle().subpageText
            local f    = function ( add )
                             local s = add
                             for i = 1, #r do
                                 if r[ i ] == s then
                                     s = false
                                     break -- for i
                                 end
                             end -- for i
                             if s then
                                 table.insert( r, s )
                             end
                         end
            r = { }
            if sub:find( "/", 2, true ) then
                sub = sub:match( "/(%l%l%l?)$" )
                if sub then
                    table.insert( r, sub )
                end
            elseif sub:find( "^%l%l%l?%-?%a?%a?%a?%a?$" )  and
                   mw.language.isSupportedLanguage( sub ) then
                table.insert( r, sub )
            end
            f( self )
            f( "en" )
            Multilingual.polyglott = r
        end
        return r
    end -- favorites()
    
    
    
    local feasible = function ( ask, accept )
        -- Is ask to be supported by application?
        -- Precondition:
        --     ask     -- lowercase code
        --     accept  -- sequence table, with offered lowercase codes
        -- Postcondition:
        --     nil, or true
        local r
        for i = 1, #accept do
            if accept[ i ] == ask then
                r = true
                break -- for i
            end
        end -- for i
        return r
    end -- feasible()
    
    
    
    local fetch = function ( access, append )
        -- Attach config or library module
        -- Precondition:
        --     access  -- module title
        --     append  -- string, with subpage part of this; or false
        -- Postcondition:
        --     Returns:  table, with library, or false
        local got, sign
        if append then
            sign = string.format( "%s/%s", access, append )
        else
            sign = access
        end
        if type( Multilingual.ext ) ~= "table" then
            Multilingual.ext = { }
        end
        got = Multilingual.ext[ sign ]
        if not got  and  got ~= false then
            local global = Multilingual.globals[ access ]
            local lib    = ( not append  or  append == "config" )
            got = foreignModule( access, lib, append, global )
            if type( got ) == "table" then
                if lib then
                    local startup = got[ access ]
                    if type( startup ) == "function" then
                        got = startup()
                    end
                end
            else
                got = false
            end
            Multilingual.ext[ sign ] = got
        end
        return got
    end -- fetch()
    
    
    
    local fetchISO639 = function ( access )
        -- Retrieve table from commons:Data:ISO639/***.tab
        -- Precondition:
        --     access  -- string, with subpage identification
        -- Postcondition:
        --     Returns table, with data, even empty
        local r
        if type( Multilingual.iso639 ) ~= "table" then
            Multilingual.iso639 = { }
        end
        r = Multilingual.iso639[ access ]
        if type( r ) == "nil" then
            local raw = fetchData( "ISO639/" .. access )
            if type( raw ) == "table" then
                local t
                r = { }
                for i = 1, #raw do
                    t = raw[ i ]
                    if type( t ) == "table"  and
                       type( t[ 1 ] ) == "string"  and
                       type( t[ 2 ] ) == "string" then
                        r[ t[ 1 ] ] =  t[ 2 ]
                    else
                        break -- for i
                    end
                end -- for i
            else
                r = false
            end
            Multilingual.iso639[ access ] = r
        end
        return r or { }
    end -- fetchISO639()
    
    
    
    local fill = function ( access, alien, frame )
        -- Expand language name template
        -- Precondition:
        --     access  -- string, with language code
        --     alien   -- language code for which to be generated
        --     frame   -- frame, if available
        -- Postcondition:
        --     Returns string
        local template = Multilingual.tmplLang
        local r
        if type( template ) ~= "table" then
            local cnf = fetch( "Multilingual", "config" )
            if cnf then
                template = cnf.tmplLang
            end
        end
        if type( template ) == "table" then
            local source = template.title
            local f, lucky, s
            Multilingual.tmplLang = template
            if type( source ) ~= "string"  and
               type( template.namePat ) == "string"  and
               template.namePat:find( "%s", 1, true ) then
                source = string.format( template.namePat, access )
            end
            if type( source ) == "string" then
                if not Multilingual.frame then
                    if frame then
                        Multilingual.frame = frame
                    else
                        Multilingual.frame = mw.getCurrentFrame()
                    end
                end
                f = function ( a )
                        return Multilingual.frame:expandTemplate{ title = a }
                    end
                lucky, s = pcall( f, source )
                if lucky then
                    r = s
                end
            end
        end
        return r
    end -- fill()
    
    
    
    local find = function ( ask, alien )
        -- Derive language code from name
        -- Precondition:
        --     ask    -- language name, downcased
        --     alien  -- language code of ask
        -- Postcondition:
        --     nil, or string
        local codes = mw.language.fetchLanguageNames( alien, "all" )
        local r
        for k, v in pairs( codes ) do
            if mw.ustring.lower( v ) == ask then
                r = k
                break -- for k, v
            end
        end -- for k, v
        if not r then
            r = Multilingual.fair( ask )
        end
        return r
    end -- find()
    
    
    
    local fold = function ( frame )
        -- Merge template and #invoke arglist
        -- Precondition:
        --     frame   -- template frame
        -- Postcondition:
        --     table, with combined arglist
        local r = { }
        local f = function ( apply )
                      if type( apply ) == "table"  and
                         type( apply.args ) == "table" then
                          for k, v in pairs( apply.args ) do
                              v = mw.text.trim( v )
                              if v ~= "" then
                                  r[ tostring( k ) ] = v
                              end
                          end -- for k, v
                      end
                  end -- f()
        f( frame:getParent() )
        f( frame )
        return r
    end -- fold()
    
    
    
    User.favorize = function ( accept, frame )
        -- Guess user language
        -- Precondition:
        --     accept  -- sequence table, with offered ISO 639 etc. codes
        --     frame   -- frame, if available
        -- Postcondition:
        --     Returns string with best code, or nil
        if not ( User.self or User.langs ) then
            if not User.trials then
                User.tell = mw.message.new( User.sniffer )
                if User.tell:exists() then
                    User.trials = { }
                    if not Multilingual.frame then
                        if frame then
                            Multilingual.frame = frame
                        else
                            Multilingual.frame = mw.getCurrentFrame()
                        end
                    end
                    User.sin = Multilingual.frame:callParserFunction( "int",
                                                               User.sniffer )
                else
                    User.langs = true
                end
            end
            if User.sin then
                local order  = { }
                local post   = { }
                local three  = { }
                local unfold = { }
                local s, sin
                for i = 1, #accept do
                    s = accept[ i ]
                    if not User.trials[ s ] then
                        if #s > 2 then
                            if s:find( "-", 3, true ) then
                                table.insert( unfold, s )
                            else
                                table.insert( three, s )
                            end
                        else
                            if Multilingual.prefer[ s ] then
                                table.insert( order, s )
                            else
                                table.insert( post, s )
                            end
                        end
                    end
                end -- for i
                for i = 1, #post do
                    table.insert( order, post[ i ] )
                end -- for i
                for i = 1, #three do
                    table.insert( order, three[ i ] )
                end -- for i
                for i = 1, #unfold do
                    table.insert( order, unfold[ i ] )
                end -- for i
                for i = 1, #order do
                    s = order[ i ]
                    sin = User.tell:inLanguage( s ):plain()
                    if sin == User.sin then
                        User.self = s
                        break -- for i
                    else
                        User.trials[ s ] = true
                    end
                end -- for i
            end
        end
        return User.self
    end -- User.favorize()
    
    
    
    Multilingual.fair = function ( ask )
        -- Format language specification according to RFC 5646 etc.
        -- Precondition:
        --     ask  -- string or table, as created by .getLang()
        -- Postcondition:
        --     Returns string, or false
        local s = type( ask )
        local q, r
        if s == "table" then
            q = ask
        elseif s == "string" then
            q = Multilingual.getLang( ask )
        end
        if q  and
           q.legal  and
           mw.language.isKnownLanguageTag( q.base ) then
            r = q.base
            if q.n > 1 then
                local order = { "extlang",
                                "script",
                                "region",
                                "other",
                                "extension" }
                for i = 1, #order do
                    s = q[ order[ i ] ]
                    if s then
                        r =  string.format( "%s-%s", r, s )
                    end
                end -- for i
            end
        end
        return r or false
    end -- Multilingual.fair()
    
    
    
    Multilingual.fallback = function ( able, another )
        -- Is another language suitable as replacement?
        -- Precondition:
        --     able     -- language version specifier to be supported
        --     another  -- language specifier of a possible replacement,
        --                 or not to retrieve a fallback table
        -- Postcondition:
        --     Returns boolean, or table with fallback codes
        local r
        if type( able ) == "string"  and  #able > 0 then
            if type( another ) == "string"  and  #another > 0 then
                if able == another then
                    r = true
                else
                    local s = Multilingual.getBase( able )
                    if s == another then
                        r = true
                    else
                        local others = mw.language.getFallbacksFor( s )
                        r = feasible( another, others )
                    end
                end
            else
                local s = Multilingual.getBase( able )
                if s then
                    r = mw.language.getFallbacksFor( s )
                    if r[ 1 ] == "en" then
                        local d = fetchISO639( "fallback" )
                        if type( d ) == "table"  and
                           type( d[ s ] ) == "string" then
                            r = mw.text.split( d[ s ], "|" )
                            table.insert( r, "en" )
                        end
                    end
                end
            end
        end
        return r or false
    end -- Multilingual.fallback()
    
    
    
    Multilingual.findCode = function ( ask )
        -- Retrieve code of local (current project or English) language name
        -- Precondition:
        --     ask  -- string, with presumable language name
        --             A code itself will be identified, too.
        -- Postcondition:
        --     Returns string, or false
        local seek = mw.text.trim( ask )
        local r = false
        if #seek > 1 then
            if seek:find( "[", 1, true ) then
                local wlink = fetch( "WLink" )
                if wlink  and
                   type( wlink.getPlain ) == "function" then
                    seek = wlink.getPlain( seek )
                end
            end
            seek = mw.ustring.lower( seek )
            if Multilingual.isLang( seek ) then
                r = Multilingual.fair( seek )
            else
                local collection = favorites()
                for i = 1, #collection do
                    r = find( seek, collection[ i ] )
                    if r then
                        break -- for i
                    end
                end -- for i
            end
        end
        return r
    end -- Multilingual.findCode()
    
    
    
    Multilingual.fix = function ( attempt )
        -- Fix frequently mistaken language code
        -- Precondition:
        --     attempt  -- string, with presumable language code
        -- Postcondition:
        --     Returns string with correction, or false if no problem known
        local r = fetchISO639( "correction" )[ attempt:lower() ]
        return r or false
    end -- Multilingual.fix()
    
    
    
    Multilingual.format = function ( apply, alien, alter, active, alert,
                                     frame, assembly, adjacent, ahead )
        -- Format one or more languages
        -- Precondition:
        --     apply     -- string with language list or item
        --     alien     -- language of the answer
        --                  -- nil, false, "*": native
        --                  -- "!": current project
        --                  -- "#": code, downcased, space separated
        --                  -- "-": code, mixcase, space separated
        --                  -- any valid code
        --     alter     -- capitalize, if "c"; downcase all, if "d"
        --                  capitalize first item only, if "f"
        --                  downcase every first word only, if "m"
        --     active    -- link items, if true
        --     alert     -- string with category title in case of error
        --     frame     -- if available
        --     assembly  -- string with split pattern, if list expected
        --     adjacent  -- string with list separator, else assembly
        --     ahead     -- string to prepend first element, if any
        -- Postcondition:
        --     Returns string, or false if apply empty
        local r = false
        if apply then
            local slang
            if assembly then
                local bucket = mw.text.split( apply, assembly )
                local shift = alter
                local separator
                if adjacent then
                    separator = adjacent
                elseif alien == "#"  or  alien == "-" then
                    separator = " "
                else
                    separator = assembly
                end
                for k, v in pairs( bucket ) do
                    slang = Multilingual.format( v, alien, shift, active,
                                                 alert )
                    if slang then
                        if r then
                            r = string.format( "%s%s%s",
                                               r, separator, slang )
                        else
                            r = slang
                            if shift == "f" then
                                shift = "d"
                            end
                        end
                    end
                end -- for k, v
                if r and ahead then
                    r = ahead .. r
                end
            else
                local single = mw.text.trim( apply )
                if single == "" then
                    r = false
                else
                    local lapsus, slot
                    slang = Multilingual.findCode( single )
                    if slang then
                        if alien == "-" then
                            r = slang
                        elseif alien == "#" then
                            r = slang:lower()
                        else
                            r = Multilingual.getName( slang, alien )
                            if active then
                                slot = fill( slang, false, frame )
                                if slot then
                                    local wlink = fetch( "WLink" )
                                    if wlink  and
                                       type( wlink.getTarget )
                                                           == "function" then
                                        slot = wlink.getTarget( slot )
                                    end
                                else
                                    lapsus = alert
                                end
                            end
                        end
                    else
                        r = single
                        if active then
                            local title = mw.title.makeTitle( 0, single )
                            if title.exists then
                                slot = single
                            end
                        end
                        lapsus = alert
                    end
                    if not r then
                        r = single
                    elseif alter == "c" or alter == "f" then
                        r = mw.ustring.upper( mw.ustring.sub( r, 1, 1 ) )
                            .. mw.ustring.sub( r, 2 )
                    elseif alter == "d" then
                        if Multilingual.isMinusculable( slang, r ) then
                            r = mw.ustring.lower( r )
                        end
                    elseif alter == "m" then
                        if Multilingual.isMinusculable( slang, r ) then
                            r = mw.ustring.lower( mw.ustring.sub( r, 1, 1 ) )
                                .. mw.ustring.sub( r, 2 )
                        end
                    end
                    if slot then
                        if r == slot then
                            r = string.format( "[[%s]]", r )
                        else
                            r = string.format( "[[%s|%s]]", slot, r )
                        end
                    end
                    if lapsus and alert then
                        r = string.format( "%s[[Category:%s]]", r, alert )
                    end
                end
            end
        end
        return r
    end -- Multilingual.format()
    
    
    
    Multilingual.getBase = function ( ask )
        -- Retrieve base language from possibly combined ISO language code
        -- Precondition:
        --     ask  -- language code
        -- Postcondition:
        --     Returns string, or false
        local r
        if ask then
            local slang = ask:match( "^%s*(%a%a%a?)-?%a*%s*$" )
            if slang then
                r = slang:lower()
            else
                r = false
            end
        else
            r = false
        end
        return r
    end -- Multilingual.getBase()
    
    
    
    Multilingual.getLang = function ( ask )
        -- Retrieve components of a RFC 5646 language code
        -- Precondition:
        --     ask  -- language code with subtags
        -- Postcondition:
        --     Returns table with formatted subtags
        --             .base
        --             .region
        --             .script
        --             .suggest
        --             .year
        --             .extension
        --             .other
        --             .n
        local tags = mw.text.split( ask, "-" )
        local s    = tags[ 1 ]
        local r
        if s:match( "^%a%a%a?$" ) then
            r = { base  = s:lower(),
                  legal = true,
                  n     = #tags }
            for i = 2, r.n do
                s = tags[ i ]
                if #s == 2 then
                    if r.region  or  not s:match( "%a%a" ) then
                        r.legal = false
                    else
                        r.region = s:upper()
                    end
                elseif #s == 4 then
                    if s:match( "%a%a%a%a" ) then
                        r.legal = ( not r.script )
                        r.script = s:sub( 1, 1 ):upper() ..
                                   s:sub( 2 ):lower()
                    elseif s:match( "20%d%d" )  or
                           s:match( "1%d%d%d" ) then
                        r.legal = ( not r.year )
                        r.year = s
                    else
                        r.legal = false
                    end
                elseif #s == 3 then
                    if r.extlang  or  not s:match( "%a%a%a" ) then
                        r.legal = false
                    else
                        r.extlang = s:lower()
                    end
                elseif #s == 1 then
                    s = s:lower()
                    if s:match( "[tux]" ) then
                        r.extension = s
                        for k = i + 1, r.n do
                            s = tags[ k ]
                            if s:match( "^%w+$" ) then
                                r.extension = string.format( "%s-%s",
                                                             r.extension, s )
                            else
                                r.legal = false
                            end
                        end -- for k
                    else
                        r.legal = false
                    end
                    break -- for i
                else
                    r.legal = ( not r.other )  and
                              s:match( "%a%a%a" )
                    r.other = s:lower()
                end
                if not r.legal then
                    break -- for i
                end
            end -- for i
            if r.legal then
                r.suggest = Multilingual.fix( r.base )
                if r.suggest then
                    r.legal = false
                end
            end
        else
            r = { legal = false }
        end
        if not r.legal then
            local cnf = fetch( "Multilingual", "config" )
            if cnf  and  type( cnf.scream ) == "string" then
                r.scream = cnf.scream
            end
        end
        return r
    end -- Multilingual.getLang()
    
    
    
    Multilingual.getName = function ( ask, alien )
        -- Which name is assigned to this language code?
        -- Precondition:
        --     ask    -- language code
        --     alien  -- language of the answer
        --               -- nil, false, "*": native
        --               -- "!": current project
        --               -- any valid code
        -- Postcondition:
        --     Returns string, or false
        local r
        if ask then
            local slang   = alien
            local tLang
            if slang then
                if slang == "*" then
                    slang = Multilingual.fair( ask )
                elseif slang == "!" then
                    slang = favorites()[ 1 ]
                else
                    slang = Multilingual.fair( slang )
                end
            else
                slang = Multilingual.fair( ask )
            end
            if not slang then
                slang = ask or "?????"
            end
            slang = slang:lower()
            tLang = fetch( "Multilingual", "names" )
            if tLang then
                tLang = tLang[ slang ]
                if tLang then
                    r = tLang[ ask ]
                end
            end
            if not r then
                if not Multilingual.ext.tMW then
                    Multilingual.ext.tMW = { }
                end
                tLang = Multilingual.ext.tMW[ slang ]
                if tLang == nil then
                    tLang = mw.language.fetchLanguageNames( slang )
                    if tLang then
                        Multilingual.ext.tMW[ slang ] = tLang
                    else
                        Multilingual.ext.tMW[ slang ] = false
                    end
                end
                if tLang then
                    r = tLang[ ask ]
                end
            end
            if not r then
                r = mw.language.fetchLanguageName( ask:lower(), slang )
                if r == "" then
                    r = false
                end
            end
        else
            r = false
        end
        return r
    end -- Multilingual.getName()
    
    
    
    Multilingual.i18n = function ( available, alt, frame )
        -- Select translatable message
        -- Precondition:
        --     available  -- table, with mapping language code ./. text
        --     alt        -- string|nil|false, with fallback text
        --     frame      -- frame, if available
        --     Returns
        --         1. string|nil|false, with selected message
        --         2. string|nil|false, with language code
        local r1, r2
        if type( available ) == "table" then
            local codes = { }
            local trsl  = { }
            local slang
            for k, v in pairs( available ) do
                if type( k ) == "string"  and
                   type( v ) == "string" then
                    slang = mw.text.trim( k:lower() )
                    table.insert( codes, slang )
                    trsl[ slang ] = v
                end
            end -- for k, v
            slang = Multilingual.userLang( codes, frame )
            if slang  and  trsl[ slang ] then
                r1 = mw.text.trim( trsl[ slang ] )
                if r1 == "" then
                    r1 = false
                else
                    r2 = slang
                end
            end
        end
        if not r1  and  type( alt ) == "string" then
            r1 = mw.text.trim( alt )
            if r1 == "" then
                r1 = false
            end
        end
        return r1, r2
    end -- Multilingual.i18n()
    
    
    
    Multilingual.int = function ( access, alien, apply )
        -- Translated system message
        -- Precondition:
        --     access  -- message ID
        --     alien   -- language code
        --     apply   -- nil, or sequence table with parameters $1, $2, ...
        -- Postcondition:
        --     Returns string, or false
        local o = mw.message.new( access )
        local r
        if o:exists() then
            if type( alien ) == "string" then
                o:inLanguage( alien:lower() )
            end
            if type( apply ) == "table" then
                o:params( apply )
            end
            r = o:plain()
        end
        return r or false
    end -- Multilingual.int()
    
    
    
    Multilingual.isLang = function ( ask, additional )
        -- Could this be an ISO language code?
        -- Precondition:
        --     ask         -- language code
        --     additional  -- true, if Wiki codes like "simple" permitted
        -- Postcondition:
        --     Returns boolean
        local r, s
        if additional then
            s = ask
        else
            s = Multilingual.getBase( ask )
        end
        if s then
            r = mw.language.isKnownLanguageTag( s )
            if r then
                r = not Multilingual.fix( s )
            elseif additional then
                r = Multilingual.exotic[ s ] or false
            end
        else
            r = false
        end
        return r
    end -- Multilingual.isLang()
    
    
    
    Multilingual.isLangWiki = function ( ask )
        -- Could this be a Wiki language version?
        -- Precondition:
        --     ask  -- language version specifier
        -- Postcondition:
        --     Returns boolean
        local r
        local s = Multilingual.getBase( ask )
        if s then
            r = mw.language.isSupportedLanguage( s )  or
                Multilingual.exotic[ ask ]
        else
            r = false
        end
        return r
    end -- Multilingual.isLangWiki()
    
    
    
    Multilingual.isMinusculable = function ( ask, assigned )
        -- Could this language name become downcased?
        -- Precondition:
        --     ask       -- language code, or nil
        --     assigned  -- language name, or nil
        -- Postcondition:
        --     Returns boolean
        local r = true
        if ask then
            local cnf = fetch( "Multilingual", "config" )
            if cnf then
                local s = string.format( " %s ", ask:lower() )
                if type( cnf.stopMinusculization ) == "string"
                   and  cnf.stopMinusculization:find( s, 1, true ) then
                    r = false
                end
                if r  and  assigned
                   and  type( cnf.seekMinusculization ) == "string"
                   and  cnf.seekMinusculization:find( s, 1, true )
                   and  type( cnf.scanMinusculization ) == "string" then
                    local scan = assigned:gsub( "[%(%)]", " " ) .. " "
                    if not scan:find( cnf.scanMinusculization ) then
                        r = false
                    end
                end
            end
        end
        return r
    end -- Multilingual.isMinusculable()
    
    
    
    Multilingual.isRTL = function ( ask )
        -- Check whether language is written right-to-left
        -- Precondition:
        --     ask  -- string, with language (or script) code
        -- Returns true, if right-to-left
        local r
        Multilingual.rtl = Multilingual.rtl or { }
        r = Multilingual.rtl[ ask ]
        if type( r ) ~= "boolean" then
            local bib = fetch( "ISO15924" )
            if type( bib ) == "table"  and
               type( bib.isRTL ) == "function" then
                r = bib.isRTL( ask )
            else
                r = mw.language.new( ask ):isRTL()
            end
            Multilingual.rtl[ ask ] = r
        end
        return r
    end -- Multilingual.isRTL()
    
    
    
    Multilingual.message = function ( arglist, frame )
        -- Show text in best match of user language like system message
        -- Precondition:
        --     arglist  -- template arguments
        --     frame    -- frame, if available
        -- Postcondition:
        --     Returns string with appropriate text
        local r
        if type( arglist ) == "table" then
            local t = { }
            local m, p, save
            for k, v in pairs( arglist ) do
                if type( k ) == "string"  and
                   type( v ) == "string" then
                    v = mw.text.trim( v )
                    if v ~= "" then
                        if k:match( "^%l%l" ) then
                            t[ k ] = v
                        elseif k:match( "^%$%d$" )  and  k ~= "$0" then
                            p = p or { }
                            k = tonumber( k:match( "^%$(%d)$" ) )
                            p[ k ] = v
                            if not m  or  k > m then
                                m = k
                            end
                        end
                    end
                end
            end -- for k, v
            if type( arglist[ "-" ] ) == "string" then
                save = arglist[ arglist[ "-" ] ]
            end
            r = Multilingual.i18n( t, save, frame )
            if p  and  r  and  r:find( "$", 1, true ) then
                t = { }
                for i = 1, m do
                    t[ i ] = p[ i ]  or  ""
                end -- for i
                r = mw.message.newRawMessage( r, t ):plain()
            end
        end
        return r  or  ""
    end -- Multilingual.message()
    
    
    
    Multilingual.sitelink = function ( all, frame )
        -- Make link at local or other site with optimal linktext translation
        -- Precondition:
        --     all    -- string or table or number, item ID or entity
        --     frame  -- frame, if available
        -- Postcondition:
        --     Returns string with any helpful internal link, or plain text
        local s = type( all )
        local object, r
        if s == "table" then
            object = all
        elseif s == "string" then
            object = mw.wikibase.getEntity( all )
        elseif s == "number" then
            object = mw.wikibase.getEntity( string.format( "Q%d", all ) )
        end
        if type( object ) == "table" then
            local collection = object.sitelinks
            local entry
            s = false
            if type( collection ) == "table" then
                Multilingual.site = Multilingual.site  or
                                    mw.wikibase.getGlobalSiteId()
                entry = collection[ Multilingual.site ]
                if entry then
                    s = ":" .. entry.title
                elseif collection.enwiki then
                    s = "w:en:" .. collection.enwiki.title
                end
            end
            r = Multilingual.wikibase( object, "labels", frame )
            if s then
                if s == ":" .. r then
                    r = string.format( "[[%s]]", s )
                else
                    r = string.format( "[[%s|%s]]", s, r )
                end
            end
        end
        return r  or  ""
    end -- Multilingual.sitelink()
    
    
    
    Multilingual.tabData = function ( access, at, alt, frame )
        -- Retrieve translated keyword from commons:Data:****.tab
        -- Precondition:
        --     access  -- string, with page identification on Commons
        --     at      -- string, with keyword
        --     alt     -- string|nil|false, with fallback text
        --     frame   -- frame, if available
        --     Returns
        --         1. string|nil|false, with selected message
        --         2. language code, or "error"
        local data = fetchData( access )
        local r1, r2
        if  type( data ) == "table" then
            if type( at ) == "string" then
                local seek = mw.text.trim( at )
                if seek == "" then
                    r1 = "EMPTY Multilingual.tabData key"
                else
                    local e, poly
                    for i = 1, #data do
                        e = data[ i ]
                        if type( e ) == "table" then
                            if e[ 1 ] == seek then
                                if type( e[ 2 ] ) == "table" then
                                    poly = e[ 2 ]
                                else
                                    r1 = "INVALID Multilingual.tabData bad #"
                                                             .. tostring( i )
                                end
                                break   -- for i
                            end
                        else
                            break   -- for i
                        end
                    end   -- for i
                    if poly then
                        data = poly
                    else
                        r1 = "UNKNOWN Multilingual.tabData key: " .. seek
                    end
                end
            else
                r1 = "INVALID Multilingual.tabData key"
            end
        else
            r1 = data
        end
        if r1 then
            r2 = "error"
        elseif data then
            r1, r2 = Multilingual.i18n( data, alt, frame )
            r2 = r2 or "error"
        end
        return r1, r2
    end -- Multilingual.tabData()
    
    
    
    Multilingual.userLang = function ( accept, frame )
        -- Try to support user language by application
        -- Precondition:
        --     accept  -- string or table
        --                space separated list of available ISO 639 codes
        --                Default: project language, or English
        --     frame   -- frame, if available
        -- Postcondition:
        --     Returns string with appropriate code
        local s = type( accept )
        local codes, r, slang
        if s == "string" then
            codes = mw.text.split( accept:lower(), "%s+" )
        elseif s == "table" then
            codes = { }
            for i = 1, #accept do
                s = accept[ i ]
                if type( s ) == "string"  and
                   s ~= "" then
                    table.insert( codes, s:lower() )
                end
            end -- for i
        end
        slang = User.favorize( codes, frame )
        if slang then
            if feasible( slang, codes ) then
                r = slang
            elseif slang:find( "-", 1, true ) then
                slang = Multilingual.getBase( slang )
                if feasible( slang, codes ) then
                    r = slang
                end
            end
            if not r then
                local others = mw.language.getFallbacksFor( slang )
                for i = 1, #others do
                    slang = others[ i ]
                    if feasible( slang, codes ) then
                        r = slang
                        break -- for i
                    end
                end -- for i
            end
        end
        if not r then
            local back = favorites()
            for i = 1, #back do
                slang = back[ i ]
                if feasible( slang, codes ) then
                    r = slang
                    break -- for i
                end
            end -- for i
            if not r  and  codes[ 1 ] then
                r = codes[ 1 ]
            end
        end
        return r  or  favorites()[ 1 ]
    end -- Multilingual.userLang()
    
    
    
    Multilingual.userLangCode = function ()
        -- Guess a user language code
        -- Postcondition:
        --     Returns code of current best guess
        return User.self  or  favorites()[ 1 ]
    end -- Multilingual.userLangCode()
    
    
    
    Multilingual.wikibase = function ( all, about, attempt, frame )
        -- Optimal translation of wikibase component
        -- Precondition:
        --     all      -- string or table, object ID or entity
        --     about    -- boolean, true "descriptions" or false "labels"
        --     attempt  -- string or not, code of preferred language
        --     frame    -- frame, if available
        -- Postcondition:
        --     Returns
        --         1. string, with selected message
        --         2. string, with language code, or not
        local s = type( all )
        local object, r, r2
        if s == "table" then
            object = all
        elseif s == "string" then
            object = mw.wikibase.getEntity( all )
        end
        if type( object ) == "table" then
            if about  and  about ~= "labels" then
                s = "descriptions"
            else
                s = "labels"
            end
            object = object[ s ]
            if type( object ) == "table" then
                if object[ attempt ] then
                    r  = object[ attempt ].value
                    r2 = attempt
                else
                    local poly
                    for k, v in pairs( object ) do
                        poly = poly or { }
                        poly[ k ] = v.value
                    end -- for k, v
                    if poly then
                        r, r2 = Multilingual.i18n( poly, nil, frame )
                    end
                end
            end
        end
        return r  or  "",   r2
    end -- Multilingual.wikibase()
    
    
    
    Failsafe.failsafe = function ( atleast )
        -- Retrieve versioning and check for compliance
        -- Precondition:
        --     atleast  -- string, with required version
        --                         or wikidata|item|~|@ or false
        -- Postcondition:
        --     Returns  string  -- with queried version/item, also if problem
        --              false   -- if appropriate
        -- 2020-08-17
        local since = atleast
        local last    = ( since == "~" )
        local linked  = ( since == "@" )
        local link    = ( since == "item" )
        local r
        if last  or  link  or  linked  or  since == "wikidata" then
            local item = Failsafe.item
            since = false
            if type( item ) == "number"  and  item > 0 then
                local suited = string.format( "Q%d", item )
                if link then
                    r = suited
                else
                    local entity = mw.wikibase.getEntity( suited )
                    if type( entity ) == "table" then
                        local seek = Failsafe.serialProperty or "P348"
                        local vsn  = entity:formatPropertyValues( seek )
                        if type( vsn ) == "table"  and
                           type( vsn.value ) == "string"  and
                           vsn.value ~= "" then
                            if last  and  vsn.value == Failsafe.serial then
                                r = false
                            elseif linked then
                                if mw.title.getCurrentTitle().prefixedText
                                   ==  mw.wikibase.getSitelink( suited ) then
                                    r = false
                                else
                                    r = suited
                                end
                            else
                                r = vsn.value
                            end
                        end
                    end
                end
            end
        end
        if type( r ) == "nil" then
            if not since  or  since <= Failsafe.serial then
                r = Failsafe.serial
            else
                r = false
            end
        end
        return r
    end -- Failsafe.failsafe()
    
    
    
    -- Export
    local p = { }
    
    
    
    p.fair = function ( frame )
        -- Format language code
        --     1  -- language code
        local s = mw.text.trim( frame.args[ 1 ]  or  "" )
        return Multilingual.fair( s )  or  ""
    end -- p.fair
    
    
    
    p.fallback = function ( frame )
        -- Is another language suitable as replacement?
        --     1  -- language version specifier to be supported
        --     2  -- language specifier of a possible replacement
        local s1 = mw.text.trim( frame.args[ 1 ]  or  "" )
        local s2 = mw.text.trim( frame.args[ 2 ]  or  "" )
        local r  = Multilingual.fallback( s1, s2 )
        if type( r ) == "table" then
            r = r[ 1 ]
        else
            r = r  and  "1"   or   ""
        end
        return r
    end -- p.fallback
    
    
    
    p.findCode = function ( frame )
        -- Retrieve language code from language name
        --     1  -- name in current project language
        local s = mw.text.trim( frame.args[ 1 ]  or  "" )
        return Multilingual.findCode( s )  or  ""
    end -- p.findCode
    
    
    
    p.fix = function ( frame )
        local r = frame.args[ 1 ]
        if r then
            r = Multilingual.fix( mw.text.trim( r ) )
        end
        return r or ""
    end -- p.fix
    
    
    
    p.format = function ( frame )
        -- Format one or more languages
        --     1          -- language list or item
        --     slang      -- language of the answer, if not native
        --                   * -- native
        --                   ! -- current project
        --                   any valid code
        --     shift      -- capitalize, if "c"; downcase, if "d"
        --                   capitalize first item only, if "f"
        --     link       -- 1 -- link items
        --     scream     -- category title in case of error
        --     split      -- split pattern, if list expected
        --     separator  -- list separator, else split
        --     start      -- prepend first element, if any
        local r
        local link
        if frame.args.link == "1" then
            link = true
        end
        r = Multilingual.format( frame.args[ 1 ],
                                 frame.args.slang,
                                 frame.args.shift,
                                 link,
                                 frame.args.scream,
                                 frame,
                                 frame.args.split,
                                 frame.args.separator,
                                 frame.args.start )
        return r or ""
    end -- p.format
    
    
    
    p.getBase = function ( frame )
        -- Retrieve base language from possibly combined ISO language code
        --     1  -- code
        local s = mw.text.trim( frame.args[ 1 ]  or  "" )
        return Multilingual.getBase( s )  or  ""
    end -- p.getBase
    
    
    
    p.getName = function ( frame )
        -- Retrieve language name from ISO language code
        --     1  -- code
        --     2  -- language to be used for the answer, if not native
        --           ! -- current project
        --           * -- native
        --           any valid code
        local s     = mw.text.trim( frame.args[ 1 ]  or  "" )
        local slang = frame.args[ 2 ]
        local r
        Multilingual.frame = frame
        if slang then
            slang = mw.text.trim( slang )
        end
        r = Multilingual.getName( s, slang )
        return r or ""
    end -- p.getName
    
    
    
    p.int = function ( frame )
        -- Translated system message
        --     1             -- message ID
        --     lang          -- language code
        --     $1, $2, ...   -- parameters
        local sysMsg = frame.args[ 1 ]
        local r
        if sysMsg then
            sysMsg = mw.text.trim( sysMsg )
            if sysMsg ~= "" then
                local n     = 0
                local slang = frame.args.lang
                local i, params, s
                if slang == "" then
                    slang = false
                end
                for k, v in pairs( frame.args ) do
                    if type( k ) == "string" then
                        s = k:match( "^%$(%d+)$" )
                        if s then
                            i = tonumber( s )
                            if i > n then
                                n = i
                            end
                        end
                    end
                end -- for k, v
                if n > 0 then
                    local s
                    params = { }
                    for i = 1, n do
                        s = frame.args[ "$" .. tostring( i ) ]  or  ""
                        table.insert( params, s )
                    end -- for i
                end
                r = Multilingual.int( sysMsg, slang, params )
            end
        end
        return r or ""
    end -- p.int
    
    
    
    p.isLang = function ( frame )
        -- Could this be an ISO language code?
        --     1  -- code
        local s = mw.text.trim( frame.args[ 1 ]  or  "" )
        local lucky, r = pcall( Multilingual.isLang, s )
        return r and "1" or ""
    end -- p.isLang
    
    
    
    p.isLangWiki = function ( frame )
        -- Could this be a Wiki language version?
        --     1  -- code
        -- Returns non-empty, if possibly language version
        local s = mw.text.trim( frame.args[ 1 ]  or  "" )
        local lucky, r = pcall( Multilingual.isLangWiki, s )
        return r and "1" or ""
    end -- p.isLangWiki
    
    
    
    p.isRTL = function ( frame )
        -- Check whether language is written right-to-left
        --     1  -- string, with language code
        -- Returns non-empty, if right-to-left
        local s = mw.text.trim( frame.args[ 1 ]  or  "" )
        return Multilingual.isRTL( s ) and "1" or ""
    end -- p.isRTL()
    
    
    
    p.message = function ( frame )
        -- Translation of text element
        return Multilingual.message( fold( frame ), frame )
    end -- p.message
    
    
    
    p.sitelink = function ( frame )
        -- Make link at local or other site with optimal linktext translation
        --     1  -- item ID
        local s = mw.text.trim( frame.args[ 1 ]  or  "" )
        local r
        if s:match( "^%d+$") then
            r = tonumber( s )
        elseif s:match( "^Q%d+$") then
            r = s
        end
        if r then
            r = Multilingual.sitelink( r, frame )
        end
        return r or s
    end -- p.sitelink
    
    
    
    p.tabData = function ( frame )
        -- Retrieve best message text from Commons Data
        --     1    -- page identification on Commons
        --     2    -- keyword
        --     alt  -- fallback text
        local suite = frame.args[ 1 ]
        local seek  = frame.args[ 2 ]
        local salt  = frame.args.alt
        local r     = Multilingual.tabData( suite, seek, salt, frame )
        return r
    end -- p.tabData
    
    
    
    p.userLang = function ( frame )
        -- Which language does the current user prefer?
        --     1  -- space separated list of available ISO 639 codes
        local s = mw.text.trim( frame.args[ 1 ]  or  "" )
        return Multilingual.userLang( s, frame )
    end -- p.userLang
    
    
    
    p.wikibase = function ( frame )
        -- Optimal translation of wikibase component
        --     1  -- object ID
        --     2  -- 1 for "descriptions", 0 for "labels".
        --           or either "descriptions" or "labels"
        local r
        local s = mw.text.trim( frame.args[ 1 ]  or  "" )
        if s ~= "" then
            local s2    = mw.text.trim( frame.args[ 2 ]  or  "0" )
            local slang = mw.text.trim( frame.args.lang  or  "" )
            local large = ( s2 ~= ""  and  s2 ~= "0" )
            if slang == "" then
                slang = false
            end
            r = Multilingual.wikibase( s, large, slang, frame )
        end
        return r or ""
    end -- p.wikibase
    
    
    
    p.failsafe = function ( frame )
        -- Versioning interface
        local s = type( frame )
        local since
        if s == "table" then
            since = frame.args[ 1 ]
        elseif s == "string" then
            since = frame
        end
        if since then
            since = mw.text.trim( since )
            if since == "" then
                since = false
            end
        end
        return Failsafe.failsafe( since )  or  ""
    end -- p.failsafe()
    
    
    
    p.Multilingual = function ()
        return Multilingual
    end -- p.Multilingual
    
    return p