Module:Fallback/sandbox

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
Lua

CodeDiscussionEditHistoryLinksLink count Subpages:DocumentationTestsResultsSandboxLive code All modules

This is sandbox for Module:Fallback.

Code

--[[
  __  __           _       _        _____     _ _ _                _    
 |  \/  | ___   __| |_   _| | ___ _|  ___|_ _| | | |__   __ _  ___| | __
 | |\/| |/ _ \ / _` | | | | |/ _ (_) |_ / _` | | | '_ \ / _` |/ __| |/ /
 | |  | | (_) | (_| | |_| | |  __/_|  _| (_| | | | |_) | (_| | (__|   < 
 |_|  |_|\___/ \__,_|\__,_|_|\___(_)_|  \__,_|_|_|_.__/ \__,_|\___|_|\_\
 
 Authors and maintainers:
* User:Zolo - original version.
* User:Jarekt 
* User:Verdy_p - updated for stricter compliance with BCP 47 for the support
  of fallbacks (see talk page)
]]
--[==[
These are source lists of fallbacks available for language `lang` :
* Local source (on Commons):
  _fblist[lang] = mw.loadData('Module:Fallbacklist')[lang]
* Builtin source of MediaWiki:
  mw.language.getFallbacksFor(lang)

None of these two sources define a root "default" (which is implicit at end of
these lists). These sources are also still not recursively expanded for BCP 47
conformance.

Beside the "default", there are supplementary languages to try (the default
language of the wiki, and "en" for MediaWiki itself). However MediaWiki
implicitly adds "en" at end of all its returned lists (meant to be used for
its own UI whose source of translations is always English), it has to be
removed temporarily before the expansion (if the source language is not "en"
or "en-*"), and then added back again (if it has not been removed) after this
expansion and after adding the "default".

To solve this build and export a getFallbacksFor(lang) function in that module,
to replace the function with same signature from the builtin Mediawiki module.
]==]
local _fblist = mw.loadData('Module:Fallbacklist')

--[==[
_removevalue
	Utility: generic function missing in the standard `table` library of Lua.
Parameters:
	t - the array to scan and modify.
	value - the value of array entries to remove.
]==]
local function _removevalue(t, value)
	for k, v in ipairs(t) do
		if v == value then
			table.remove(t, k)
		end
	end
end

--[==[
_findvalue
	Utility: generic function missing in the standard `table` library of Lua.
Parameters:
	t - the array to scan.
	value - the value of array entries to locate.
Returns:
	nil if not found, or the key of the first occurence found.
]==]
local function _findvalue(t, value)
	for k, v in ipairs(t) do
		if v == value then
			return k
		end
	end
	return nil
end

--[==[
_undesiredTruncatedFallbacks
	List of undesired truncated fallbacks to remove or to truncate more. All
	other truncated language codes are assumed to be useful for translations.
	Note that the mapped codes may be valid under BCP47 (except those ending
	in '-x') but not useful for translations (they are language families,
	not invidual languages or macrolanguages for which we search fallback
	translations); we don't support them.
]==]
local _undesiredTruncatedFallbacks = {
	['bat'] = '', -- e.g. 'bat-smg'
	['fiu'] = '', -- e.g. 'fiu-vro'
	['map'] = '', -- e.g. 'map-bms'
	['roa'] = '', -- e.g. 'roa-rup', 'roa-tara'
	['zh-min'] = 'zh', -- e.g. 'zh-min-nan'
}

--[==[
getFallbacksFor (alias getfblist)
	Expand a language code in string to an array of language codes with their
	fallbacks. Similar to mw.language.getFallbacksFor(lang) but also takes
	input from the Commons fallback chain and process them in a BCP 47
	conforming way. The returned array will contain the given language, then
	their fallbacks, adding also their default BCP 47 fallbacks when language
	codes are variants, then 'default', and 'en', without any duplicate
	language code.
Parameters:
	lang - desired language (often user's native language).
Returns:
	An array of language code expanded by recursively adding their fallbacks
	extracted from the local `Module:Fallbacklist` of Commons or from
	MediaWiki builtin fallbacks.
Error handling:
]==]
local _fallbacks = {} -- Cache for speed.
local function getFallbacksFor(lang)
	if _fallbacks[lang] then -- In cache?
		return _fallbacks[lang]
	end
	-- 1. First expand the list with Commons-local fallbacks from _fblist[lang].
	local languages, result, hasEnglish = {lang}, {}, false
	while true do
		-- Extract a language from the array to process from the start.
		local lang = table.remove(languages, 1)
		if lang == nil then
			break
		end
		-- Normalize the given language code to lower case, with dashes separators only, and all spaces trimmed
		lang = string.lower(lang):gsub('_', '-'):gsub(' ','')
		-- Insert it (only once) at end of the result array.
		if _findvalue(result, lang) == nil then
			table.insert(result, lang)
		end
		-- Check if English (or a variant of English) is in source languages: in that case,
		-- the next processing loop will not ignore 'en' found in MediaWiki builtin fallbacks
		-- so it will be found before the final 'default'.
		if lang == 'en' or lang:sub(1, 3) == 'en-' then
			hasEnglish = true
		end
		-- Enumerate its local local fallbacks and process them.
		local fallbacks = _fblist[lang]
		if fallbacks then
			for _, lang in ipairs(fallbacks) do
				-- Don't need to process again a language already in the result list.
				if _findvalue(result, lang) == nil then
					-- Don't process now a language still in the list to process
					-- (otherwise the while loop would be infinite).
					if _findvalue(languages, lang) == nil then
						table.insert(languages, lang)
					end
				end
			end
		end
	end
	-- 2. Same thing but expand the list for BCP 47 conformance where it contains language variants with '-'.
	languages, result = result, {}
	while true do
		-- Extract a language from the array to process from the start.
		local lang = table.remove(languages, 1)
		if lang == nil then
			break
		end
		-- Insert it (only once) at end of the result array.
		if _findvalue(result, lang) == nil then
			table.insert(result, lang)
		end
		-- Enumerate its default BCP47 fallbacks (if lang is a variant) and process them.
		local fallbacks
		while lang ~= '' do
			lang, fallbacks = string.gsub(lang, "%-[%*0-9a-z]*$", "")
			-- No truncated fallback found, or undesired truncated fallbacks (to language families only).
			if fallbacks == 0 then
				break
			else
				-- Truncated fallback possible, but must be truncated a bit more (there were several variant extensions).
				if lang:sub(-2, -2) == '-' then -- E.g. ending with '-i', or '-x', or '-*', or '--'.
					lang = lang:sub(1, -3) -- Drop the last 2 characters.
				elseif _undesiredTruncatedFallbacks[lang] ~= nil then
					lang = _undesiredTruncatedFallbacks[lang]
				end
				-- Other truncations are safe and recommended (generally these are variants for region codes or script codes after the base language)
			end
			-- Empty truncated language or wildcard (added to BCP47)
			-- Don't need to process again a language already in the result list.
			if lang ~= '' and lang ~= '*' and _findvalue(result, lang) == nil then
				-- Don't process now a language still in the list to process
				-- (otherwise the while loop would be infinite).
				if _findvalue(languages, lang) == nil then
					table.insert(languages, lang)
				end
			end
		end
	end
	-- 3. Same thing but process it now with MediaWiki fallbacks from mw.language.getFallbacksFor(lang).
	languages, result = result, {}
	while true do
		-- Extract a language from the array to process from the start.
		local lang = table.remove(languages, 1)
		if lang == nil then
			break
		end
		-- Insert it (only once) at end of the result array.
		if _findvalue(result, lang) == nil then
			table.insert(result, lang)
		end
		-- Eumerate its MediaWiki fallbacks and process them.
		local fallbacks = mw.language.getFallbacksFor(lang)
		if fallbacks then
			for _, lang in ipairs(fallbacks) do
				-- MediaWiki includes 'en' at end of all lists, discard it for now unless source language is English
				if lang ~= 'en' or hasEnglish then
					-- Don't need to process again a language already in the result list.
					if _findvalue(result, lang) == nil then
						-- Don't process now a language still in the list to process
						-- (otherwise the while loop would be infinite).
						if _findvalue(languages, lang) == nil then
							table.insert(languages, lang)
						end
					end
				end
			end
		end
	end
	-- 4. Same thing but expand the list for BCP 47 conformance where it contains language variants with '-'.
	languages, result = result, {}
	while true do
		-- Extract a language from the array to process from the start.
		local lang = table.remove(languages, 1)
		if lang == nil then
			break
		end
		-- Insert it (only once) at end of the result array.
		if _findvalue(result, lang) == nil then
			table.insert(result, lang)
		end
		-- Enumerate its default BCP47 fallbacks (if lang is a variant) and process them.
		local fallbacks
		while true do
			lang, fallbacks = string.gsub(lang, "%-%w*$", "")
			-- no truncated fallback found, or undesired truncated fallbacks (to language families only)
			if fallbacks == 0 or _undesiredTruncatedFallbacks[lang] == '' then
				break
			-- truncated fallback possible, but must be truncated a bit more (there were several variant extensions)
			elseif _undesiredTruncatedFallbacks[lang] ~= nil then
				lang = _undesiredTruncatedFallbacks[lang]
			-- other truncations are safe and recommended (generally these are variants for region codes or script codes after the base language)
			end
			-- Don't need to process again a language already in the result list.
			if _findvalue(result, lang) == nil then
				-- Don't process now a language still in the list to process
				-- (otherwise the while loop would be infinite).
				if _findvalue(languages, lang) == nil then
					table.insert(languages, lang)
				end
			end
		end
	end
	-- 5. Finally add the 'default'.
	if _findvalue(result, 'default') == nil then
		table.insert(result, 'default')
	end
	-- 6. We may want to add here the default language 'xx' of the local wiki (when it is not English, e.g. in Wikipedia).
	-- May be we have a variable in the `mw` environment for this code, instead of editing the two occurences of 'xx' below.
	--[==[
	if _findvalue(result, 'xx') == nil then
		table.insert(result, 'xx')
	end
	--]==]
	-- 7. Add 'en' as the last fallback (ignored when processing the Mediawiki list).
	if _findvalue(result, 'en') == nil then
		table.insert(result, 'en')
	end
	_fallbacks[lang] = result -- Store in cache.
	return result
end

--[==[
_langSwitch
	This function is the core part of the LangSwitch template.
Example usage from Lua:
	text = _langSwitch({
			en = 'text in English',
			['bs hr'] = 'bosanski ili hrvatski tekst',
			pl = 'tekst po Polsku'
		}, lang)
Parameters:
	args - table with translations by language.
	lang - desired language (often user's native language).
Error handling:
]==]
local function _langSwitch(args, lang, quick, nocat) -- args: table of translations.
	-- Get the desired language (either stated one or user's default language).
	if not lang then -- Must become proper error.
		return '<strong class="error">LangSwitch Error: no lang</strong>'
	end
	-- Expand the table of translation when it has argument named with multiple valid language codes
	-- (like 'de/gsw' or 'de, gsw') for mapping the same text to several languages codes. These codes
	-- are not case-significant and can only contain letters, digits, hyphens or underscores.
	-- These codes are normalized to lowercase letters, digits and hyphens for matching from fallback chains.
	local args1 = {}
	for key, value in pairs(args) do
		if value ~= '' and type(key) == 'string'
		and not(key == 'lang' or key == 'quick' or key == 'nocat') then
			for code in key:lower():gsub('_', '-'):gmatch('[%*%-0-9a-z]+') do
				args1[code] = value
			end
		end
	end
	args = args1
	-- Return error if there is not default and no English version.
	if not(args.en or args.default) then
		local err = '<strong class="error">LangSwitch Error: no default</strong>'
		return (nocat and err) or (err .. '[[Category:LangSwitch template without default version]]')
	end
	-- To improve performance try quick switch, and load fallback chain only if needed. 
	-- In the vast majority of cases quick switch (not using any fallbacks) is sufficient.
	local value = args[lang]
	if value and value ~= '' or quick then
		return (value ~= '~' and value) or ''
	end
	-- Get the list of acceptable language (lang + those in lang's fallback chain) and check their content.
	local langList = getFallbacksFor(lang)
	for _, language in ipairs(langList) do
		value = args[language] 
		if value and value ~= '' then
			return (value ~= '~' and value) or ''
		end
	end
end

--[==[
translatelua
	Allows easy translation or internalization of pages in Lua, using a data module
	containing a table of translation strings per language, or several tables of
	translations in a table keyed by a message identifier.
Example usage from a template:
	{{#invoke:Fallback|translatelua| i18n/oil on canvas|lang={{{lang|}}}}}
Parameters:
	frame.args[1] - (required) name of translation module.
	frame.args[2] - (optional) message identifier to use in Module:[frame.args[1]].
	frame.args.lang - (optional) desired language (often user's native language).
Error handling:
	In the specified data module, the selected translation table must contain
	at least a 'default' or 'en' field for the default translation.
	Errors are returned as HTML strong elements with class="error", and
	optionally in a tracking category (unless the nocat parameter is set).
]==]
local function translatelua(frame) -- Version to be used from wikitext.
	local args = frame.args
	-- If no expected args provided, then check in parent template/module frame.
	if not args or not(args[1] or args.lang or args.quick or args.nocat) then
		args = mw.getCurrentFrame():getParent().args
	end
	local page = mw.loadData('Module:' .. mw.text.trim(args[1])) -- Data should only contain a table of translations.
	if args[2] then -- Message identifier to use if the data module is a table of tables of translations.
		page = page[mw.text.trim(args[2])]
	end
	local lang, quick, nocat = args.lang, args.quick, args.nocat
	args.lang, args.quick, args.nocat = nil
	if not lang or mw.text.trim(lang) == '' then
		lang = frame:callParserFunction('Int', 'Lang') -- Get user's chosen language.
	end
	lang = lang:lower()
	return _langSwitch(page, lang, quick, nocat)
end

--[==[
langSwitch
	This function is the core part of the LangSwitch template.
Example usage from a template:
	{{#invoke:Fallback|langSwitch|en=text in english|pl=tekst po polsku|lang={{int:lang}} }}
Parameters:
	frame.args - table with translations by language.
	frame.args.lang - (optional) desired language (often user's native language).
	frame.args.default - (optional) default translation (required if frame.args.en is not set).
	frame.args.en - (optional) English translation (required if frame.args.default is not set).
Error handling:
]==]
local function langSwitch(frame) -- Version to be used from wikitext.
	local args = frame.args
	-- If no expected args provided, then check parent template/module args.
	if not args or not(args.default or args.en or args.lang or args.quick or args.nocat) then
		args = mw.getCurrentFrame():getParent().args
	end
	local lang, quick, nocat = args.lang, args.quick, args.nocat
	args.lang, args.quick, args.nocat = nil
	if not lang or mw.text.trim(lang) == '' then
		lang = frame:callParserFunction('Int', 'Lang') -- Get user's chosen language.
	end
	lang = lang:lower()
	return _langSwitch(args, lang, args.quick, args.nocat)
end

--[==[
autotranslate
	This function is the core part of the Autotranslate template.
Usage from a template:
	{{#invoke:Fallback|autotranslate|base=|lang=}}
Parameters:
	frame.args.base - base page name.
	frame.args.lang - desired language (often user's native language).
Error handling:
]==]
local function autotranslate(frame) -- Version to be used from wikitext.
	local args = frame.args
	-- If no expected args provided, then check in parent template/module frame.
	if not args or not(args.lang or args.base) then
		args = mw.getCurrentFrame():getParent().args
	end
	local lang, base = args.lang, args.base
	-- Find base page.
	if not base or base == '' then
		return '<strong class="error">Base page not provided for autotranslate</strong>'
	end
	if not string.find(base, ':') then -- If base page does not indicate a namespace (can be a leading ':'),
		base = 'Template:' .. base     -- then assume it is a template.
	end
	if not lang or mw.text.trim(lang) == '' then
		lang = frame:callParserFunction('Int', 'Lang') -- Get user's chosen language.
	end
	lang = lang:lower()
	local langList = getFallbacksFor(lang)
	-- Find base template language subpage.
	local page = nil
	for _, language in ipairs(langList) do
		if mw.title.new(base .. '/' .. language).exists then
			page =  base .. '/' .. language -- Returns only the page.
			break
		end
	end
	if not page then
		return string.format('<strong class="error">no fallback page found for Autotranslate (base=[[%s]], lang=%s)</strong>', args.base, args.lang)
	end
	-- Repack args in a standard table.
	local inArgs = {}
	for key, value in pairs(args) do
		inArgs[key] = value;
	end
	-- Transclude {{page|....}} with template arguments the same as the ones passed to {{Autotranslate}} template.
	inArgs.base = nil
	return frame:expandTemplate{ title = page, args = inArgs }
end

-- exports
local p = {
	_undesiredTruncatedFallbacks = _undesiredTruncatedFallbacks,
	getFallbacksFor = getFallbacksFor,
	fblist = getFallbacksFor, -- (alias kept for compatiblity before checking if it's needed)
	_langSwitch = _langSwitch,
	langSwitch = langSwitch,
	translatelua = translatelua,
	autotranslate = autotranslate,
}
setmetatable(p, {
	-- This function must be named quickTests().
	quickTests = function()
		-- TODO. For now look at {{FULLPAGENAME}}/testcases and results in {{TALKPAGENAME}}/testcases
		return true
	end
})
return p