Module:Foreign names
From the Super Mario Wiki, the Mario encyclopedia
Jump to navigationJump to search
This module creates the {{foreign names}} table. It uses the languages defined here.
-- Module:Foreign names
-- Lua replacement for Template:Foreign names
--
-- Parameters:
-- origin Prefix of the language listed first in the table (default
-- "Jpn"). Every other language follows in the order given in
-- languages.json. An unrecognised value displays the table in
-- languages.json's own order instead, with nothing moved to the
-- top.
local p = {}
-- Language definitions loaded from subpage (prefix, name, BCP 47 tag, dir, latin, variant)
local languages = mw.loadJsonData("Module:Foreign names/languages.json")
-- Prefix of the language shown first when no "origin" parameter is given.
local DEFAULT_ORIGIN = "Jpn"
-- Namespaces treated as "content" for categorization purposes (0 = main,
-- 102 = Gallery, 106 = Multimedia).
local CONTENT_NAMESPACES = { [0] = true, [102] = true, [106] = true }
-- Strip HTML tags (used to clean display names for category sort keys)
local function stripTags(s)
return (s:gsub("<[^>]+>", ""))
end
-- Return the parameter key for entry index idx under a given prefix.
-- Index 1 -> bare prefix ("Jpn")
-- Index 2+ -> prefix .. index ("Jpn2", "Jpn3", ...)
local function paramKey(prefix, idx)
if idx == 1 then return prefix end
return prefix .. idx
end
-- Count how many consecutive value entries exist for a prefix.
local function countEntries(args, prefix)
if not args[prefix] or args[prefix] == "" then return 0 end
local n = 1
while true do
local val = args[paramKey(prefix, n + 1)]
if not val or val == "" then break end
n = n + 1
end
return n
end
-- Resolve a parameter for entry idx, walking back through "^" inherit
-- markers to the nearest real value. suffix = "" (base name) | "R" |
-- "RM" | "RC" | "RH" | "M" | "N" | "C" — used for the name itself, the
-- romanisations, the Meaning/Note columns, and citations.
--
-- Two inherit forms are recognised:
-- "^" Bare caret: inherit from the previous entry under the SAME
-- prefix and suffix (e.g. Jpn2M=^ pulls JpnM). Walks back
-- through a run of bare carets to the nearest real value.
-- "^Name" Explicit caret: inherit the raw value of parameter Name
-- directly (e.g. FreEM=^FreAM), resolved in a single hop —
-- if Name's own value is itself a "^..." reference, that is
-- returned as-is rather than followed further (this also
-- means a bare-caret walk-back that lands on a stored
-- "^Name" value gets that one hop of resolution applied).
local function resolveR(args, prefix, idx, suffix)
local val = args[paramKey(prefix, idx) .. suffix] or ""
-- Walk back through bare "^" inherit markers within the same prefix.
local i = idx
while val == "^" and i > 1 do
i = i - 1
val = args[paramKey(prefix, i) .. suffix] or ""
end
if val == "^" then val = "" end
-- Explicit "^Name" reference: resolved in a single hop.
if val ~= "" and val:sub(1, 1) == "^" then
val = args[val:sub(2)] or ""
end
return val
end
-- Return the primary BCP 47 subtag for a language tag — everything before
-- the first "-" (or the whole tag if there is none). This is used as a
-- "same language family" key: French Canada/Europe are both tagged "fr",
-- Spanish Latin America/Europe both "es", and Chinese Simplified/Traditional
-- are "zh-Hans"/"zh-Hant", which both reduce to "zh". Unrelated languages in
-- languages.json don't collide on this (no two unrelated entries share a
-- primary subtag today), so it reliably identifies regional/script variants
-- of the SAME language without needing any extra data in languages.json.
local function langFamily(tag)
return tag:match("^[^%-]+") or tag
end
-- ---------------------------------------------------------------------------
-- Language ordering
-- ---------------------------------------------------------------------------
-- Return the language definitions in display order: the origin language
-- first, then every other language in the order given by languages.json.
-- origin is matched against each definition's prefix, ignoring case. If it
-- matches nothing, languages.json's own order is returned unchanged (no
-- language moved to the top).
local function orderLanguages(origin)
local function findIndex(prefix)
prefix = prefix:lower()
for i, langDef in ipairs(languages) do
if langDef.prefix:lower() == prefix then
return i
end
end
return nil
end
local originIdx = findIndex(origin)
if not originIdx then
return languages
end
local ordered = {}
ordered[1] = languages[originIdx]
for i, langDef in ipairs(languages) do
if i ~= originIdx then
ordered[#ordered + 1] = langDef
end
end
return ordered
end
-- ---------------------------------------------------------------------------
-- Table-wide rowspan merging
-- ---------------------------------------------------------------------------
-- Given a list of merge KEYS and a parallel list of EMPTY flags, both in
-- final output-row order, compute a rowspan merge plan: consecutive rows
-- with an equal key, neither flagged empty, are merged into a single cell
-- spanning all of them. Two rows flagged empty are never merged, since that
-- would draw a meaningless empty cell across unrelated rows. This is a
-- generic algorithm — see buildMergeKeys and its callers in p.main for what
-- the keys actually mean (language family, resolved value, etc).
--
-- Returns an array `plan` the same length as `keys`: plan[i] is the rowspan
-- to use when emitting row i's cell, or `false` when row i's cell is
-- absorbed into an earlier row's rowspan and should be skipped entirely.
local function computeSpanPlan(keys, empty)
local plan = {}
local n = #keys
local i = 1
while i <= n do
if empty[i] then
plan[i] = 1
i = i + 1
else
local k = keys[i]
local span = 1
local j = i + 1
while j <= n and not empty[j] and keys[j] == k do
span = span + 1
j = j + 1
end
plan[i] = span
for x = i + 1, i + span - 1 do
plan[x] = false
end
i = i + span
end
end
return plan
end
-- Build the (keys, empty) pair computeSpanPlan needs for one mergeable
-- column. `valueOf(row)` returns that row's raw value for the column; the
-- key is that value scoped to the row's language family (see langFamily),
-- so rows only merge with others in the same family, and a row is flagged
-- empty exactly when its raw value is "" (a composite value like the Name
-- column's "dir\1content" is never legitimately empty, so it never gets
-- flagged — matching the old per-column behavior without special-casing it
-- here).
local function buildMergeKeys(rows, valueOf)
local keys, empty = {}, {}
for i, row in ipairs(rows) do
local v = valueOf(row)
keys[i] = row.family .. "\1" .. v
empty[i] = v == ""
end
return keys, empty
end
-- Test whether a string contains non-ASCII characters, indicating a non-Latin
-- script that needs romanisation. Only called for languages without the latin
-- flag, so we don't need to worry about accented Latin characters.
-- Certain non-ASCII ranges are stripped before checking:
-- U+FF01–U+FF5E Fullwidth ASCII variants (e.g. A, 3)
-- U+2000–U+2BFF Symbols and punctuation (e.g. ☆, →, …)
-- U+3000–U+303F CJK punctuation (e.g. 。、「」)
local function needsRomanisation(s)
local normalized = s
:gsub("\239\188[\129-\191]", "x") -- U+FF01–U+FF3F
:gsub("\239\189[\128-\158]", "x") -- U+FF40–U+FF5E
:gsub("\226[\128-\175][\128-\191]", "x") -- U+2000–U+2BFF
:gsub("\227\128[\128-\191]", "x") -- U+3000–U+303F
return normalized:find("[\128-\255]") ~= nil
end
-- Wrap text in a lang-tagged span.
local function langSpan(tag, text)
return '<span lang="' .. tag .. '">' .. text .. '</span>'
end
-- ---------------------------------------------------------------------------
-- Cell formatting helpers
-- ---------------------------------------------------------------------------
-- Build a "<td>" (optionally "<td rowspan="...">", optionally carrying extra
-- attributes such as dir=) wrapping content.
local function tdCell(content, span, attrs)
local attrStr = attrs and (' ' .. attrs) or ''
if span and span > 1 then
return '<td rowspan="' .. span .. '"' .. attrStr .. '>' .. content .. '</td>'
end
return '<td' .. attrStr .. '>' .. content .. '</td>'
end
-- Append a "<br />''text''" romanisation fragment to nameParts, optionally
-- labelled (e.g. "(Mandarin)"). No-op when text is empty.
local function appendRomanisation(nameParts, nw, text, label)
if text == "" then return end
local part = "<br />" .. nw("''" .. text .. "''")
if label then part = part .. " <small>(" .. label .. ")</small>" end
table.insert(nameParts, part)
end
-- Append a category link when a name is missing a romanisation it needs
-- (content namespace, non-Latin script, and the name actually needs one).
local function appendMissingRomanisationCategory(nameParts, isContent, isLatin, value, displayClean)
if isContent and not isLatin and needsRomanisation(value) then
table.insert(nameParts, "[[Category:Articles with missing foreign name romanizations|" .. displayClean .. "]]")
end
end
-- Content for a Meaning cell given its resolved value.
local function meaningContent(value, displayClean, isContent)
if value == "" then
local cat = isContent
and ("[[Category:Articles with unknown foreign name meanings|" .. displayClean .. "]]")
or ""
return '<span class="foreign-meaning">?</span>' .. cat
elseif value == "-" then
return '<span class="no-foreign-meaning">-</span>'
else
return '<span class="foreign-meaning">' .. value .. '</span>'
end
end
-- Content for a Note cell given its resolved value.
local function noteContent(value)
if value ~= "" then
return '<span class="foreign-note"><small>' .. value .. '</small></span>'
else
return '<span class="no-foreign-note"></span>'
end
end
-- Content for a Reference cell given its resolved citation value.
local function refContentFor(citation, displayClean, isContent, nocitMode, nw)
if citation ~= "" then
return '<span class="foreign-ref">' .. nw(citation) .. '</span>'
elseif nocitMode then
return '<span class="no-foreign-ref"></span>'
else
-- nocitMode is only false when isContent is true, so this is
-- always a content-namespace page missing a citation.
local cat = '[[Category:Articles with unsourced ' .. displayClean .. ' names]]'
return '<sup><abbr title="Citation needed">[?]</abbr></sup>' .. cat
end
end
-- ---------------------------------------------------------------------------
-- Row collection
-- ---------------------------------------------------------------------------
-- Builds one row descriptor per entry of `langDef` and appends them to the
-- shared, table-wide `rows` array. Unlike a fully self-contained per-language
-- row builder, this does NOT decide any rowspans itself: it only resolves
-- and records each row's Name/Meaning/Note/Reference values. All rowspan
-- merging happens afterwards, once, across the complete `rows` list — see
-- computeSpanPlan — so that adjacent rows from DIFFERENT languages can merge
-- with each other, not just repeat entries within one language.
local function collectLangRows(rows, args, langDef, ctx, nw)
local prefix = langDef.prefix
local display = langDef.name
local langTag = langDef.tag
local dir = langDef.dir or "ltr"
local isLatin = langDef.latin ~= false
local variant = langDef.variant
local isContent = ctx.isContent
local total = countEntries(args, prefix)
if total == 0 then return end
local displayClean = stripTags(display)
for idx = 1, total do
-- Base name value: also goes through resolveR so it can be
-- inherited via bare "^" or an explicit "^Name" reference,
-- same as the R/M/N/C fields below.
local value = resolveR(args, prefix, idx, "")
-- ----------------------------------------------------------------
-- Name cell content
-- ----------------------------------------------------------------
local nameParts = {}
local isChinese = variant == "chinese" or variant == "chineseT"
-- Resolve the Chinese romanisation fields once, through the "^"
-- inheritance chain, up front — they're needed both for the lang-tag
-- decision below and for the romanisation lines further down, and
-- checking the raw param would treat an unresolved "^" as "present"
-- even when it ultimately inherits an empty value.
local chRM, chRC, chRH, chR
if isChinese then
chRM = resolveR(args, prefix, idx, "RM")
chRC = resolveR(args, prefix, idx, "RC")
chRH = resolveR(args, prefix, idx, "RH")
chR = resolveR(args, prefix, idx, "R")
end
-- Determine the lang subtag (ChiT may need -HK for Cantonese-only).
local ltag = langTag
if variant == "chineseT" and chRC ~= "" and chRM == "" and chR == "" and chRH == "" then
ltag = "zh-Hant-HK"
end
table.insert(nameParts, nw(langSpan(ltag, value)))
if isChinese then
-- Chinese: separate Mandarin (RM), Cantonese (RC), and Hokkien (RH) romanisations
appendRomanisation(nameParts, nw, chRM, "Mandarin")
appendRomanisation(nameParts, nw, chRC, "Cantonese")
appendRomanisation(nameParts, nw, chRH, "Hokkien")
if chR ~= "" then
appendRomanisation(nameParts, nw, chR, nil)
elseif chRM == "" and chRC == "" and chRH == "" then
appendMissingRomanisationCategory(nameParts, isContent, isLatin, value, displayClean)
end
else
local r = resolveR(args, prefix, idx, "R")
if r ~= "" then
appendRomanisation(nameParts, nw, r, nil)
else
appendMissingRomanisationCategory(nameParts, isContent, isLatin, value, displayClean)
end
end
local row = {
displayClean = displayClean,
family = langFamily(langTag),
nameContent = table.concat(nameParts),
dir = dir,
mValue = resolveR(args, prefix, idx, "M"),
nValue = resolveR(args, prefix, idx, "N"),
citation = resolveR(args, prefix, idx, "C"),
}
-- Language label cell: only on the first entry, spans all of this
-- language's own rows (never merges with a different language).
if idx == 1 then
row.langLabelCell = tdCell(nw(display), total)
end
table.insert(rows, row)
end
end
-- ---------------------------------------------------------------------------
-- Main entry point
-- ---------------------------------------------------------------------------
function p.main(frame)
-- Support both transclusion (parent frame) and direct invocation
local parent = frame:getParent()
local args = parent and parent.args or frame.args
local title = mw.title.getCurrentTitle()
local isContent = CONTENT_NAMESPACES[title.namespace] == true
local ctx = {
isContent = isContent,
nocitMode = (args["citations"] or "") ~= "" or not isContent,
wrapMode = (args["wrap"] or "") ~= "",
}
-- Language listed first in the table (see orderLanguages).
local origin = args["origin"]
if not origin or origin == "" then
origin = DEFAULT_ORIGIN
end
-- Wrap text in a nowrap span (disabled when wrap mode is active). This is
-- a table-wide setting, so one instance is shared by every language.
local function nw(text)
if ctx.wrapMode then return text end
return '<span class="nowrap">' .. text .. '</span>'
end
-- Phase 1: collect every row's data across all languages, without
-- deciding any rowspans yet.
local rows = {}
for _, langDef in ipairs(orderLanguages(origin)) do
collectLangRows(rows, args, langDef, ctx, nw)
end
-- Phase 2: compute rowspan merge plans across the WHOLE table for each
-- mergeable column. Every merge key is prefixed with the row's language
-- family (see langFamily), so a merge can only happen between repeat
-- entries of one language or between regional/script variants of the
-- SAME language (e.g. FreA/FreE, SpaA/SpaE, ChiS/ChiT) — never between
-- two unrelated languages that just happen to share a value. The Name
-- column additionally requires the writing direction and rendered
-- content to match exactly, which in practice fires for FreA/FreE and
-- SpaA/SpaE (both tag their name "fr"/"es" regardless of region) but not
-- for scripts that carry different tags even with matching visible text
-- (e.g. zh-Hans vs zh-Hant).
local nameSpans = computeSpanPlan(buildMergeKeys(rows, function(row) return row.dir .. "\1" .. row.nameContent end))
local mSpans = computeSpanPlan(buildMergeKeys(rows, function(row) return row.mValue end))
local nSpans = computeSpanPlan(buildMergeKeys(rows, function(row) return row.nValue end))
-- Phase 3: emit the table, skipping any cell absorbed into an earlier
-- row's rowspan.
local out = {}
table.insert(out, '<table id="foreign-names" class="wikitable">')
table.insert(out, '<tr><th>Language</th><th>Name</th><th>Meaning</th><th>Note(s)</th>'
.. '<th><abbr title="Reference(s)">Ref.</abbr></th></tr>')
for i, row in ipairs(rows) do
local cells = {}
if row.langLabelCell then
table.insert(cells, row.langLabelCell)
end
if nameSpans[i] ~= false then
table.insert(cells, tdCell(row.nameContent, nameSpans[i], 'dir="' .. row.dir .. '"'))
end
if mSpans[i] ~= false then
table.insert(cells, tdCell(meaningContent(row.mValue, row.displayClean, ctx.isContent), mSpans[i]))
end
if nSpans[i] ~= false then
table.insert(cells, tdCell(noteContent(row.nValue), nSpans[i]))
end
table.insert(cells, tdCell(refContentFor(row.citation, row.displayClean, ctx.isContent, ctx.nocitMode, nw)))
table.insert(out, '<tr>' .. table.concat(cells) .. '</tr>')
end
table.insert(out, '</table>')
return table.concat(out, "\n")
end
return p