Module:Common
Module:Common
Shared stateless helper functions, which can be loaded in other Lua modules via:
local common = require("Module:Common")
Usage
This module also exposes a few permanent utility entry points.
Get URL-safe version of a given string
MessageBundle keys are built from a slugified (URL-safe) version of a string. Use this to check the exact slug a given input will produce.
If no value is specified, it defaults to the current page's title:
{{#invoke:Common|checkSlug}}
A specific string can also be provided instead:
{{#invoke:Common|checkSlug|Terrarium of Muad'Dib}}
Get an auto-localised string from a MessageBundle
Retrieves a string for a given key from a specified MessageBundle. It auto-switches to the language that the viewer's page is currently on, and falls back to the English source if no translation exists for the current language.
{{#invoke:Common|localise|Vehicles|sandbike-name}}
Optionally, set link=true to wrap the result as a stable link to the English page name, e.g. [[Wildlife of Arrakis|Fauna von Arrakis]]:
{{#invoke:Common|localise|DLC|wildlife-of-arrakis-name|link=true}}
Resolve translation of a specified page
Checks whether a translated subpage exists for a given page title, and returns the subpage path if so, e.g. Buildables/de. If no translation exists, the original title is returned unchanged.
This is intended for building wikilinks manually, so that readers on a translated page stay in their language when following the link:
[[{{#invoke:Common|localiseTitle|Buildables|de}}|Bauwerke]]
Note that link targets which are redirects are not resolved, so their translations will not be picked up.
Parameters
checkSlug
- 1 (optional)
- string
- The string to generate a slug for.
- If omitted, defaults to the current page's title.
localise
- 1
- string
- The name of the MessageBundle to look up (without the "MessageBundle:" prefix).
- 2
- string
- The key to retrieve from the MessageBundle.
- link (optional)
- boolean
- If true, wraps the result as
[[English|Translation]]instead of just plain text. - Defaults to false.
localiseTitle
- 1
- string
- The English title of the target page.
- 2
- string
- Language code of the translated subpage to look for.
local p = {}
local tmb = require("mw.ext.translate.messageBundle")
-- ---------- String ----------
-- trims any whitespace on the edges
function p.trim(s)
return mw.text.trim(s or "")
end
-- converts string to lowercase and trims any whitespace on the edges
function p.lowerTrim(s)
return mw.ustring.lower(p.trim(s))
end
-- checks whether a string represents a truthy value; case-insensitive
function p.isTruthy(s)
s = p.lowerTrim(s)
return s == "true" or s == "yes" or s == "1"
end
-- checks whether a string represents a falsy value; case-insensitive
function p.isFalsy(s)
s = p.lowerTrim(s)
return s == "false" or s == "no" or s == "0"
end
-- checks whether boolean input was specified and returns corresponding value, else nil
function p.resolveBooleanInput(s)
if p.isTruthy(s) then return true
elseif p.isFalsy(s) then return false
else return nil end
end
-- checks whether a list of strings contains a specified token; case-insensitive
function p.listContains(list, token)
token = p.lowerTrim(token)
for _, item in ipairs(list) do
if p.lowerTrim(item) == token then return true end
end
return false
end
-- returns a URL-friendly version of a specified string
function p.slugify(name)
local s = mw.uri.decode(name, "PATH") -- decode percent-encoding
s = mw.text.decode(s) -- decode html entities
s = mw.ustring.lower(s) -- lowercase all letters
s = mw.ustring.gsub(s, "[^%w%s_]", "") -- remove all characters except alphanumerics, underscores and whitespace
s = mw.ustring.gsub(s, "%s+", "_") -- convert each group of whitespace into a single underscore
s = mw.ustring.gsub(s, "_+", "_") -- combine consecutive underscores into one
s = mw.ustring.gsub(s, "^_+", ""):gsub("_+$", "") -- trim any underscores at the edges
return s
end
-- splits a "Bundle:slug" source token into its parts
function p.parseSourceToken(token)
token = p.trim(token)
if token == "" then return nil end
return token:match("^([^:]+):(.+)$")
end
-- checks whether a source field (either a single token or a table of tokens) contains a given slug, ignoring the bundle prefix
function p.sourceMatches(source, filterSlug)
if type(source) ~= "table" then
local _, slug = p.parseSourceToken(source)
return slug == filterSlug
end
-- recurse if table
for _, token in ipairs(source) do
if p.sourceMatches(token, filterSlug) then return true end
end
return false
end
-- splits any trailing number `_<number>` off a slug/ID
-- returns base_slug, number if matched, else nil
function p.splitTrailingNumber(str)
local base, num = str:match("^(.-)_(%d+)$")
if not base then
return nil
end
return base, tonumber(num)
end
-- returns a display-ready error message, standardised for input validation checks across all modules
-- any number of extra args can be specified to be encoded and substituted into msg
function p.error(msg, ...)
if select("#", ...) > 0 then
local args = {...}
for i = 1, #args do args[i] = mw.text.encode(tostring(args[i])) end
msg = string.format(msg, unpack(args))
end
return '<strong class="error">⚠ ' .. msg .. '</strong>'
end
-- ---------- Localisation ----------
local pageLang
local bundleCache = {}
local subpageCache = {}
local function getPageLanguage()
if not pageLang then
local ok, code = pcall(mw.getCurrentFrame().preprocess, mw.getCurrentFrame(), "{{PAGELANGUAGE}}")
pageLang = mw.language.new(ok and code or mw.language.getContentLanguage():getCode())
end
return pageLang
end
local function getBundle(bundlePage, lang)
local cacheKey = bundlePage .. "|" .. tostring(lang)
if bundleCache[cacheKey] == nil then
-- if `lang` is nil, tmb.new() automatically attempts to grab a string of the invoking page's language
-- if a translation does not exist, it falls back to the English source
local ok, mb = pcall(tmb.new, bundlePage, lang)
bundleCache[cacheKey] = ok and mb or false
end
return bundleCache[cacheKey]
end
-- formats a number according to the current page's language
function p.formatNum(number)
return getPageLanguage():formatNum(number)
end
-- checks if a translation exists for a target page
-- returns the language subpage path (<target>/<lang>) if it exists, else just <target>
function p.resolveLangSubpage(target, lang)
if lang == "en" then return target end
local subpagePath = target .. "/" .. lang
local cached = subpageCache[subpagePath]
if cached ~= nil then return cached end
local subpage = mw.title.new(subpagePath)
local resolved = (subpage and subpage.exists) and subpagePath or target
subpageCache[subpagePath] = resolved
return resolved
end
-- retrieves a string value associated with a specified key from a specified MessageBundle
-- optionally wraps the result as [[<ENGLISH>|<TRANSLATION>]] if `link` is true
function p.translate(bundleName, key, lang, link, silent)
local bundlePage = "MessageBundle:" .. bundleName
local langCode = lang or getPageLanguage():getCode()
local mb = getBundle(bundlePage, lang)
if not mb then
if silent then return nil end
return p.error("MessageBundle:%s not found.", bundleName)
end
local msg = mb:t(key)
if not msg then
if silent then return nil end
return p.error("Missing key '%s' in MessageBundle:%s.", key, bundleName)
end
local translated = msg:plain()
if not link then return translated end
local mbEn = getBundle(bundlePage, "en")
local msgEn = mbEn and mbEn:t(key)
if not msgEn then
if silent then return nil end
return p.error("Missing English value for key '%s' in MessageBundle:%s.", key, bundleName)
end
local target = p.resolveLangSubpage(msgEn:plain(), langCode)
return string.format("[[%s|%s]]", target, translated)
end
-- ---------- Rendering ----------
-- keys are MessageBundles where the strings should be anchors instead of titles
-- values are the pages the anchors will be found on
local BUNDLE_REDIRECT_PAGE = {
Rewards = "Rewards",
}
-- creates a collapsible section: a heading followed by an inner content div
-- an item count is shown above the collapse toggle, and is always visible regardless of collapse state
-- mw-collapsed sections can be auto-expanded with wikilinks that target the heading ID
-- subtitle, if specified, renders above the item count
function p.newCollapsibleSection(title, count, headingLevel, expandedRaw, subtitle, anchorId)
local expanded = p.resolveBooleanInput(expandedRaw)
local outer = mw.html.create("div")
:addClass("mw-collapsible")
-- collapse the section if it contains more than 6 items (2 rows of 3 infoboxes)
-- or if expanded is specified as false
if (count > 6 and expanded == nil) or expanded == false then
outer:addClass("mw-collapsed")
end
local level = tonumber(headingLevel)
if not level or level < 0 or level > 6 then level = 3 end
-- omit heading if headingLevel was specified as 0
if level ~= 0 then
local heading = outer:tag("h" .. level):wikitext(title)
if anchorId then heading:attr("id", anchorId) end
end
if subtitle and subtitle ~= "" then
outer:tag("div")
:addClass("collapsible-subtitle")
:addClass("scalable-text")
:wikitext(subtitle)
end
outer:tag("div")
:addClass("collapsible-count")
:addClass("scalable-text")
:wikitext(string.format("Total: '''%d'''", count))
local content = outer:tag("div")
:addClass("mw-collapsible-content")
return outer, content
end
-- creates a standard sortable wikitable, ready for header/rows to be added
function p.newSortableTable()
local table_ = mw.html.create("table")
:addClass("wikitable sortable")
:css("text-align", "center")
return table_
end
-- creates a left-aligned bullet list, one <li> per item
-- if omitIfSingle is true and there is only 1 item in the list, then a plain span will be returned
function p.renderBulletList(items, omitIfSingle)
if omitIfSingle and #items == 1 then
return mw.html.create("span"):wikitext(items[1])
end
local wrapper = mw.html.create("div")
:css("display", "inline-block")
:css("text-align", "left")
local ul = wrapper:tag("ul")
for _, item in ipairs(items) do
ul:tag("li"):wikitext(item)
end
return wrapper
end
-- resolves one or more source tokens into a display string
-- token may be a single "Bundle:slug" string, or a table of such strings (for items with multiple sources)
-- single source renders as a plain line; multiple sources render as a bullet list
function p.renderSource(token)
local function keyFor(part)
return part .. "-name"
end
if type(token) == "table" then
if #token == 0 then return p.error("No source specified") end
if #token == 1 then return p.renderSource(token[1]) end
local rendered = {}
for i, t in ipairs(token) do
rendered[i] = p.renderSource(t)
end
return tostring(p.renderBulletList(rendered))
end
token = p.trim(token)
if token == "" then return p.error("No source specified") end
local bundleName, slug = p.parseSourceToken(token)
if not bundleName then return p.error("Malformed source token: %s", token) end
if not slug:find("/", 1, true) then
-- prevent Sources:Default label from becoming a wikilink
if bundleName == "Sources" and slug == "default" then
return p.translate(bundleName, keyFor(slug), nil, false)
end
-- process strings from certain MessageBundles as page anchors instead of page titles
local redirectPage = BUNDLE_REDIRECT_PAGE[bundleName]
if redirectPage then
local translated = p.translate(bundleName, keyFor(slug), nil, false)
local anchorText = p.translate(bundleName, keyFor(slug), "en", false, true)
return string.format("[[%s#%s|%s]]", redirectPage, anchorText, translated)
end
return p.translate(bundleName, keyFor(slug), nil, true)
end
-- handle contract pages (i.e. [[ContractChain/Contract|Contract]])
local parts = mw.text.split(slug, "/", true)
local targetParts = {}
for i, part in ipairs(parts) do
local resolved = p.translate(bundleName, keyFor(part), "en", false, true)
if not resolved then
return p.error("Missing key '%s' in MessageBundle:%s.", keyFor(part), bundleName)
end
targetParts[i] = resolved
end
local displayText = p.translate(bundleName, keyFor(parts[#parts]))
return string.format("[[%s|%s]]", table.concat(targetParts, "/"), displayText)
end
-- renders a version string as a wikilink to its corresponding Version page
function p.renderVersion(version)
version = p.trim(version)
if version == "" then return p.error("No version specified") end
return "[[" .. version .. "]]"
end
-- ---------- Interface ----------
-- wrapper for p.slugify(), exposed as an entry point to check URL-friendly versions of specified strings
-- this is specifically intended to help in cases where it may not be clear what IDs to use in MessageBundles
function p.checkSlug(frame)
local input = frame.args[1]
if not input or input == "" then input = mw.title.getCurrentTitle().rootText end
return p.slugify(input)
end
-- wrapper for p.translate(), exposed as an entry point for wikitext pages to grab strings from MessageBundles directly
function p.localise(frame)
local args = frame.args
return p.translate(args[1], args[2], nil, p.isTruthy(args.link))
end
-- wrapper for p.resolveLangSubpage(), exposed as an entry point for wikitext pages to resolve a page to its translated subpage if it exists
function p.localiseTitle(frame)
local args = frame.args
return p.resolveLangSubpage(args[1], args[2])
end
return p