Aktionen

Modul

Modul:Multilingual

Aus VV-WSV 1102

Version vom 5. Februar 2020, 12:54 Uhr von GlossarTOC>PerfektesChaos (2020-02-01)
(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)

local Multilingual = { suite = "Multilingual",

                      serial  = "2020-02-01",
                      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
   -- 2019-10-29
   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, 0 )
       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", slot, r )
                   end
               end
               if lapsus and alert then
                   r = string.format( "%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, 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" or "~"
   --                 or false
   -- Postcondition:
   --     Returns  string  -- with queried version, also if problem
   --              false   -- if appropriate
   -- 2019-10-15
   local last  = ( atleast == "~" )
   local since = atleast
   local r
   if last  or  since == "wikidata" then
       local item = Failsafe.item
       since = false
       if type( item ) == "number"  and  item > 0 then
           local entity = mw.wikibase.getEntity( string.format( "Q%d",
                                                                item ) )
           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
                   else
                       r = vsn.value
                   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
   return Multilingual.tabData( suite, seek, salt, frame )

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