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 got == nil 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
	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
				Multilingual.frame = frame or mw.getCurrentFrame()
			end
			f = function(a)
				return Multilingual.frame:expandTemplate{ title = a }
			end
			lucky, s = pcall(f, source)
			if lucky then
				return s
			end
		end
	end
	return nil
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
					elseif Multilingual.prefer[s] then
						table.insert(order, s)
					else
						table.insert(post, s)
					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