Dokumentation für das Modul Multilingual[Ansicht] [Bearbeiten] [Versionsgeschichte] [Aktualisieren]

Utilities for multilingual texts and ISO 639 (BCP47) issues etc.

Verwendung in anderen Modulen

Dieses Modul ist notwendig für die Ausführung folgender Module. Bei Anpassungen sollte die Funktionstüchtigkeit der folgenden Module geprüft werden. Benutze dazu auch diese Tracking-Kategorie um Fehler zu finden, die sich dann auf Artikel auswirken:

Hinweise
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