Module:headword: difference between revisions

From Wiktionary, the free dictionary
Jump to navigation Jump to search
Content deleted Content added
User:Benwing2 If data.heads is nil, make it an empty table instead of using the pagename (which is what Module:headword/templates does if no head param is provided). Also, only overwrite script table if more than 1 head or script is provided (which prevents issues when we have 0 heads but 1 script).
major rewrite; allow `data.heads` to be an object of properties (head, translit, transcription, script, qualifiers, references, etc.); lots of code cleanup and additional comments, attempt to optimize
Line 9: Line 9:


local m_data = mw.loadData("Module:headword/data")
local m_data = mw.loadData("Module:headword/data")

local title = mw.title.getCurrentTitle()


local isLemma = m_data.lemmas
local isLemma = m_data.lemmas
Line 19: Line 17:
-- If set to true, categories always appear, even in non-mainspace pages
-- If set to true, categories always appear, even in non-mainspace pages
local test_force_categories = false
local test_force_categories = false

local modules = {}

local function get_module(mod)
if not modules[mod] then
modules[mod] = require("Module:" .. mod)
end
return modules[mod]
end


-- version of rsubn() that discards all but the first return value
-- version of rsubn() that discards all but the first return value
Line 26: Line 33:
end
end


-- Add a tracking category to track entries with certain (unusually undesirable) properties. `track_id` is an identifier
local function track(page, code)
-- for the particular property being tracked and goes into the tracking page. Specifically, this adds a link in the
local tracking_page = "headword/" .. page
-- page text to [[Template:tracking/headword/TRACK_ID]], meaning you can find all entries with the `track_id` property
-- by visiting [[Special:WhatLinksHere/Template:tracking/headword/TRACK_ID]].
--
-- If `code` (a language or script code) is given, an additional tracking page
-- [[Template:tracking/headword/TRACK_ID/CODE]] is linked to, and you can find all entries in the combination of
-- `track_id` and `code` by visiting [[Special:WhatLinksHere/Template:tracking/headword/TRACK_ID/CODE]]. This makes it
-- possible to isolate only the entries with a specific tracking property that are in a given language or script.
local function track(track_id, code)
local tracking_page = "headword/" .. track_id
local m_debug_track = get_module("debug/track")
if code then
if code then
require("Module:debug/track"){tracking_page, tracking_page .. "/" .. code}
m_debug_track{tracking_page, tracking_page .. "/" .. code}
else
else
require("Module:debug/track")(tracking_page)
m_debug_track(tracking_page)
end
end
return true
return true
end
end


local function test_script(text, script_code)
if type(text) == "string" and type(script_code) == "string" then
local sc = require("Module:scripts").getByCode(script_code)
local characters
if sc then
characters = sc:getCharacters()
end


local function text_in_script(text, script_code)
local out
local sc = get_module("scripts").getByCode(script_code)
if characters then
if not sc then
text = rsub(text, "%W", "")
error("Internal error: Bad script code " .. script_code)
out = rfind(text, "[" .. characters .. "]")
end
end
local characters = sc:getCharacters()


if out then
local out
if characters then
return true
text = rsub(text, "%W", "")
else
out = rfind(text, "[" .. characters .. "]")
return false
end
end

if out then
return true
else
else
return false
mw.log("Parameters to test_script were incorrect.")
return nil
end
end
end
end
Line 67: Line 80:
local wordPunc = "-־׳״'.·*’་•:"
local wordPunc = "-־׳״'.·*’་•:"
local notWordPunc = "[^" .. wordPunc .. "]+"
local notWordPunc = "[^" .. wordPunc .. "]+"


-- Format a term (either a head term or an inflection term) along with any left or right qualifiers, references or
-- customized separator: `part` is the object specifying the term, which should optionally contain:
-- * left qualifiers in `q`, an array of strings (or `qualifiers` for compatibility purposes);
-- * right qualifiers in `qq`, an array of strings;
-- * references in `refs`, an array either of strings (formatted reference text) or objects containing fields `text`
-- (formatted reference text) and optionally `name` and/or `group`;
-- * a separator in `separator`, defaulting to " <i>or</i> " if this is not the first term (j > 1), otherwise "".
-- `formatted` is the formatted version of the term itself, and `j` is the index of the term.
local function format_term_with_qualifiers_and_refs(part, formatted, j)
local left_qualifiers, right_qualifiers
local reftext

left_qualifiers = part.q and #part.q > 0 and part.q or part.qualifiers and #part.qualifiers > 0 and part.qualifiers
if left_qualifiers then
left_qualifiers = get_module("qualifier").format_qualifier(left_qualifiers) .. " "
-- [[Special:WhatLinksHere/Template:tracking/headword/qualifier]]
track("qualifier")
end
if part.qualifiers then
-- FIXME: Eliminate these usages.
-- [[Special:WhatLinksHere/Template:tracking/headword/old-qualifier]]
track("old-qualifier")
end

right_qualifiers = part.qq and #part.qq > 0 and part.qq
if right_qualifiers then
right_qualifiers = " " .. get_module("qualifier").format_qualifier(right_qualifiers)
-- [[Special:WhatLinksHere/Template:tracking/headword/qualifier]]
track("qualifier")
end
if part.refs and #part.refs > 0 then
local refs = {}
for _, ref in ipairs(part.refs) do
if type(ref) ~= "table" then
ref = {text = ref}
end
local refargs
if ref.name or ref.group then
refargs = {name = ref.name, group = ref.group}
end
table.insert(refs, mw.getCurrentFrame():extensionTag("ref", ref.text, refargs))
end
reftext = table.concat(refs)
end

local separator = part.separator or j > 1 and " <i>or</i> " -- use "" to request no separator

if left_qualifiers then
formatted = left_qualifiers .. formatted
end
if reftext then
formatted = formatted .. reftext
end
if right_qualifiers then
formatted = formatted .. right_qualifiers
end
if separator then
formatted = separator .. formatted
end

return formatted
end




Line 102: Line 179:




local function non_categorizable()
local function non_categorizable(data)
return (title:inNamespace("") and title.text:find("^Unsupported titles/"))
return (data.title:inNamespace("") and data.title.text:find("^Unsupported titles/"))
or (title:inNamespace("Appendix") and title.text:find("^Gestures/"))
or (data.title:inNamespace("Appendix") and data.title.text:find("^Gestures/"))
end
end




-- Format a headword with transliterations.
local function preprocess(data, postype)
--[=[
[[Special:WhatLinksHere/Template:tracking/headword/heads-not-table]]
[[Special:WhatLinksHere/Template:tracking/headword/translits-not-table]]
]=]
if type(data.heads) ~= "table" then
if data.heads then
track("heads-not-table")
end

data.heads = { data.heads }
end

if type(data.translits) ~= "table" then
if data.translits then
track("translits-not-table")
end

data.translits = { data.translits }
end

if type(data.transcriptions) ~= "table" then
if data.transcriptions then
track("transcriptions-not-table")
end

data.transcriptions = { data.transcriptions }
end

if not data.heads or #data.heads == 0 then
data.heads = {""}
end

-- Determine if term is reconstructed
local is_reconstructed = data.lang:getType() == "reconstructed"
or title.nsText == "Reconstruction"

-- Create a default headword.
local subpagename = title.subpageText:gsub("^Unsupported titles/", "")
local pagename = title.text
local default_head
if is_reconstructed then
default_head = require("Module:utilities").plain_gsub(pagename, data.lang:getCanonicalName() .. "/", "")
else
default_head = subpagename
end

local unmodified_default_head = default_head

-- Add links to multi-word page names when appropriate
if not m_data.no_multiword_links[data.lang:getCode()] and not is_reconstructed and export.head_is_multiword(default_head) then
default_head = export.add_multiword_links(default_head)
end

if is_reconstructed then
default_head = "*" .. default_head
end

-- If using a discouraged character sequence, add to maintenance category
for i, script in ipairs(data.sc) do
if script:hasNormalizationFixes() == true then
if script:fixDiscouragedSequences(unfc(data.heads[i])) ~= unfc(data.heads[i]) then
table.insert(data.categories, "Pages using discouraged character sequences")
end
end
end

-- If a head is the empty string "", then replace it with the default
for i, head in ipairs(data.heads) do
if head == "" then
head = default_head
else
if head == default_head and data.lang:getCanonicalName() == "English" then
table.insert(data.categories, data.lang:getCanonicalName() .. " terms with redundant head parameter")
end
end
data.heads[i] = head
end

-- If the first head is multiword (after removing links), maybe insert into "LANG multiword terms"
if (not data.nomultiwordcat) and postype == "lemma" and data.sc[1]:hasSpaces() and (not m_data.no_multiword_cat[data.lang:getCode()]) then
-- Check for spaces or hyphens, but exclude prefixes and suffixes.
-- Use the pagename, not the head= value, because the latter may have extra
-- junk in it, e.g. superscripted text that throws off the algorithm.
local checkpattern = ".[%s%-፡]."
if m_data.hyphen_not_multiword_sep[data.lang:getCode()] then
-- Exclude hyphens if the data module states that they should for this language
checkpattern = ".[%s፡]."
end
if rfind(unmodified_default_head, checkpattern) and not non_categorizable() then
table.insert(data.categories, data.lang:getCanonicalName() .. " multiword terms")
end
end

for i, val in pairs(data.translits) do
data.translits[i] = {display = val, is_manual = true}
end

-- Make transliterations
for i, head in ipairs(data.heads) do
local translit = data.translits[i]

-- Try to generate a transliteration if necessary
if translit and translit.display == "-" then
translit = nil
elseif not translit and not notranslit[data.lang:getCode()] and data.sc[i]:isTransliterated() then
translit = (data.lang:transliterate(require("Module:links").remove_links(head), data.sc[i]))
translit = translit and mw.text.trim(translit)

-- There is still no transliteration?
-- Add the entry to a cleanup category.
if not translit then
translit = "<small>transliteration needed</small>"
table.insert(data.categories, "Requests for transliteration of " .. data.lang:getCanonicalName() .. " terms")
end

if translit then
translit = {display = translit, is_manual = false}
end
end

-- Link to the transliteration entry for languages that require this
if translit and data.lang:link_tr() then
translit.display = require("Module:links").full_link{
term = translit.display,
lang = data.lang,
sc = require("Module:scripts").getByCode("Latn"),
tr = "-"
}
end

data.translits[i] = translit
end

if data.id and type(data.id) ~= "string" then
error("The id in the data table should be a string.")
end
end


-- Format a headword with transliterations
local function format_headword(data)
local function format_headword(data)
local m_scriptutils = require("Module:script utilities")
local m_scriptutils = require("Module:script utilities")


-- Are there non-empty transliterations?
-- Are there non-empty transliterations?
-- Need to do it this way because translit[1] might be nil while translit[2] is not
local has_translits = false
local has_translits = false
local has_manual_translits = false
local has_manual_translits = false


-- Format the headwords
------ Format the headwords. ------

for i, head in ipairs(data.heads) do
local head_parts = {}
if data.translits[i] or data.transcriptions[i] then

for j, head in ipairs(data.heads) do
if head.tr or head.ts then
has_translits = true
has_translits = true
end
end
if data.translits[i] and data.translits[i].is_manual or data.transcriptions[i] then
if head.tr and head.tr.is_manual or head.ts then
has_manual_translits = true
has_manual_translits = true
end
end


local formatted
-- Apply processing to the headword, for formatting links and such
if head:find("[[", nil, true) and (not data.sc[i] or data.sc[i]:getCode() ~= "Imag") then
head = require("Module:links").language_link({term = head, lang = data.lang}, false)
else
head = data.lang:makeDisplayText(head, data.sc[i], true)
end


-- Apply processing to the headword, for formatting links and such.
-- Add language and script wrapper
if head.term:find("[[", nil, true) and head.sc:getCode() ~= "Imag" then
if i == 1 then
head = m_scriptutils.tag_text(head, data.lang, data.sc[i], "head", nil, data.id)
formatted = get_module("links").language_link({term = head.term, lang = data.lang}, false)
else
else
head = m_scriptutils.tag_text(head, data.lang, data.sc[i], "head", nil)
formatted = data.lang:makeDisplayText(head.term, head.sc, true)
end
end


-- Add language and script wrapper.
data.heads[i] = head
formatted = m_scriptutils.tag_text(formatted, data.lang, head.sc, "head", nil, j == 1 and data.id or nil)

-- Add qualifiers, references and separator.
table.insert(head_parts, format_term_with_qualifiers_and_refs(head, formatted, j))
end
end


head_parts = table.concat(head_parts)
local translits_formatted = ""


if has_manual_translits then
if has_manual_translits then
-- [[Special:WhatLinksHere/Template:tracking/headword/has-manual-translit]]
-- [[Special:WhatLinksHere/Template:tracking/headword/has-manual-translit]]
-- [[Special:WhatLinksHere/Template:tracking/headword/has-manual-translit/LANG]]
-- [[Special:WhatLinksHere/Template:tracking/headword/has-manual-translit/LANGCODE]]
track("has-manual-translit", data.lang:getCode())
track("has-manual-translit", data.lang:getCode())
end
end


------ Format the transliterations and transcriptions. ------
if has_translits then


local translits_formatted
-- Format the transliterations
local translits = data.translits
local transcriptions = data.transcriptions


if translits then
if has_translits then
local translit_parts = {}
-- using pairs() instead of ipairs() in case there is a gap
for i, _ in pairs(translits) do
for i, head in ipairs(data.heads) do
if type(i) == "number" then
if head.tr or head.ts then
local this_parts = {}
translits[i] = m_scriptutils.tag_translit(translits[i].display, data.lang:getCode(), "head", nil, translits[i].is_manual)
if head.tr then
table.insert(this_parts, m_scriptutils.tag_translit(head.tr, data.lang:getCode(), "head", nil, head.tr_manual))
if head.ts then
table.insert(this_parts, " ")
end
end
end
if head.ts then
end
table.insert(this_parts, m_scriptutils.tag_transcription(head.ts, data.lang:getCode(), "head"))
end

if transcriptions then
for i, _ in pairs(transcriptions) do
if type(i) == "number" then
transcriptions[i] = m_scriptutils.tag_transcription(transcriptions[i], data.lang:getCode(), "head")
end
end
table.insert(translit_parts, table.concat(this_parts))
end
end
end
end


translits_formatted = " (" .. table.concat(translit_parts, " <i>or</i> ") .. ")"
for i = 1, math.max(#translits, #transcriptions) do
local translits_formatted = {}
table.insert(translits_formatted, translits[i] and translits[i] or "")
table.insert(translits_formatted, (translits[i] and transcriptions[i]) and " " or "")
table.insert(translits_formatted, transcriptions[i] and "/" .. transcriptions[i] .. "/" or "")
data.translits[i] = table.concat(translits_formatted)
end

translits_formatted = " (" .. table.concat(data.translits, " <i>or</i> ") .. ")"


local transliteration_page = mw.title.new(data.lang:getCanonicalName() .. " transliteration", "Wiktionary")
local transliteration_page = mw.title.new(data.lang:getCanonicalName() .. " transliteration", "Wiktionary")
Line 332: Line 261:
end
end
end
end
else
translits_formatted = ""
end
end


------ Paste heads and transliterations/transcriptions. ------
return table.concat(data.heads, " <i>or</i> ") .. translits_formatted

return head_parts .. translits_formatted
end
end


Line 360: Line 293:
if type(part) ~= "table" then
if type(part) ~= "table" then
part = {term = part}
part = {term = part}
end

local left_qualifiers, right_qualifiers
local reftext

left_qualifiers = part.q and #part.q > 0 and part.q or part.qualifiers and #part.qualifiers > 0 and part.qualifiers
if left_qualifiers then
left_qualifiers = require("Module:qualifier").format_qualifier(left_qualifiers) .. " "

-- [[Special:WhatLinksHere/Template:tracking/headword/qualifier]]
track("qualifier")
end
right_qualifiers = part.qq and #part.qq > 0 and part.qq
if right_qualifiers then
right_qualifiers = " " .. require("Module:qualifier").format_qualifier(right_qualifiers)

-- [[Special:WhatLinksHere/Template:tracking/headword/qualifier]]
track("qualifier")
end
if part.refs and #part.refs > 0 then
local refs = {}
for _, ref in ipairs(part.refs) do
if type(ref) ~= "table" then
ref = {text = ref}
end
local refargs
if ref.name or ref.group then
refargs = {name = ref.name, group = ref.group}
end
table.insert(refs, mw.getCurrentFrame():extensionTag("ref", ref.text, refargs))
end
reftext = table.concat(refs)
end
end


Line 402: Line 303:
local nolink = part.hypothetical or part.nolink or data.nolink
local nolink = part.hypothetical or part.nolink or data.nolink


local formatted
local separator = part.separator or j > 1 and " <i>or</i> " -- use "" to request no separator

if part.label then
if part.label then
-- There should be a better way of italicizing a label. As is, this isn't customizable.
-- FIXME: There should be a better way of italicizing a label. As is, this isn't customizable.
part = "<i>" .. part.label .. "</i>"
formatted = "<i>" .. part.label .. "</i>"
else
else
-- Convert the term into a full link
-- Convert the term into a full link. Don't show a transliteration here unless enable_auto_translit is
-- requested, either at the `parts` level (i.e. per inflection) or at the `data.inflections` level (i.e.
-- Don't show a transliteration here, the consensus seems to be not to
-- specified for all inflections). This is controllable in {{head}} using autotrinfl=1 for all inflections,
-- show them in headword lines to avoid clutter.
-- or fNautotr=1 for an individual inflection (remember that a single inflection may be associated with
part = require("Module:links").full_link(
-- multiple terms). The reason for doing this is to avoid clutter in headword lines by default in languages
-- where the script is relatively straightforward to read by learners (e.g. Greek, Russian), but allow it
-- to be enabled in languages with more complex scripts (e.g. Arabic).
formatted = get_module("links").full_link(
{
{
term = not nolink and part.term or nil,
term = not nolink and part.term or nil,
alt = part.alt or (nolink and part.term or nil),
alt = part.alt or (nolink and part.term or nil),
lang = part.lang or data.lang,
lang = part.lang or data.lang,
-- FIXME, code smell in always using the first script.
sc = part.sc or parts.sc or (not part.lang and data.sc[1]),
sc = part.sc or parts.sc or (not part.lang and data.heads[1].sc),
id = part.id,
id = part.id,
genders = part.genders,
genders = part.genders,
tr = part.translit or (not (parts.enable_auto_translit or data.inflections.enable_auto_translit) and "-" or nil),
tr = part.translit or (not (parts.enable_auto_translit or data.inflections.enable_auto_translit) and "-" or nil),
ts = part.transcription,
ts = part.transcription,
accel = parts.accel or partaccel,
accel = partaccel or parts.accel,
},
},
face,
face,
Line 428: Line 333:
end
end


parts[j] = format_term_with_qualifiers_and_refs(part, formatted, j)
if left_qualifiers then
part = left_qualifiers .. part
end
if reftext then
part = part .. reftext
end
if right_qualifiers then
part = part .. right_qualifiers
end
if separator then
part = separator .. part
end

parts[j] = part
end
end


local parts_output = ""
local parts_output


if #parts > 0 then
if #parts > 0 then
Line 450: Line 342:
elseif parts.request then
elseif parts.request then
parts_output = " <small>[please provide]</small>"
parts_output = " <small>[please provide]</small>"
table.insert(data.categories, "Requests for inflections in " .. data.lang:getCanonicalName() .. " entries")
.. require("Module:utilities").format_categories(
else
{"Requests for inflections in " .. data.lang:getCanonicalName() .. " entries"},
parts_output = ""
lang,
nil,
nil,
data.force_cat_output or test_force_categories,
data.sc[1]
)
end
end


Line 463: Line 350:
end
end



-- Format the inflections following the headword
-- Format the inflections following the headword.
local function format_inflections(data)
local function format_inflections(data)
if data.inflections and #data.inflections > 0 then
if data.inflections and #data.inflections > 0 then
-- Format each inflection individually
-- Format each inflection individually.
for key, infl in ipairs(data.inflections) do
for key, infl in ipairs(data.inflections) do
data.inflections[key] = format_inflection_parts(data, infl)
data.inflections[key] = format_inflection_parts(data, infl)
Line 487: Line 375:
function export.pos_lemma_or_nonlemma(plpos, best_guess)
function export.pos_lemma_or_nonlemma(plpos, best_guess)
-- Is it a lemma category?
-- Is it a lemma category?
if isLemma[plpos] or isLemma[plpos:gsub("^reconstructed ", "")] then
if isLemma[plpos] then
return "lemma"
return "lemma"
end
local plpos_no_recon = plpos:gsub("^reconstructed ", "")
if isLemma[plpos_no_recon] then
return "lemma"
end
-- Is it a nonlemma category?
-- Is it a nonlemma category?
elseif isNonLemma[plpos]
if isNonLemma[plpos] or isNonLemma[plpos_no_recon] then
return "non-lemma form"
or isNonLemma[plpos:gsub("^reconstructed ", "")]
end
or isLemma[plpos:gsub("^mutated ", "")]
or isNonLemma[plpos:gsub("^mutated ", "")] then
local plpos_no_mut = plpos:gsub("^mutated ", "")
if isLemma[plpos_no_mut] or isNonLemma[plpos_no_mut] then
return "non-lemma form"
return "non-lemma form"
elseif best_guess then
elseif best_guess then
Line 503: Line 397:




-- Find and return the maximum index in the array `data[element]` (which may have gaps in it), and initialize it to a
local function show_headword_line(data)
-- zero-length array if unspecified. Check to make sure all keys are numeric (other than "maxindex", which is set by
local namespace = title.nsText
-- [[Module:parameters]] for list parameters), all values are strings, and unless `allow_blank_string` is given,
-- no blank (zero-length) strings are present.
local function init_and_find_maximum_index(data, element, allow_blank_string)
local maxind = 0
if not data[element] then
data[element] = {}
end
local typ = type(data[element])
if typ ~= "table" then
error(("In full_headword(), `data.%s` must be an array but is a %s"):format(element, typ))
end
for k, v in pairs(data[element]) do
if k ~= "maxindex" then
if type(k) ~= "number" then
error(("Unrecognized non-numeric key '%s' in `data.%s`"):format(k, name))
end
if k > maxind then
maxind = k
end
if v then
if type(v) ~= "string" then
error(("For key '%s' in `data.%s`, value should be a string but is a %s"):format(k, element, type(v)))
end
if not allow_blank_string and v == "" then
error(("For key '%s' in `data.%s`, blank string not allowed; use 'false' for the default"):format(k, element))
end
end
end
end
return maxind
end


--[=[
Generate a headword. This is the main function to be called externally. `data` is an object containing all the
properties needed for formatting the headword, specifically:
{
lang = LANG_OBJECT,
pagename = nil or STRING,
id = nil or STRING,
head = {
{
term = nil or "HEAD",
tr = nil or "MANUAL_TRANSLIT",
ts = nil or "MANUAL_TRANSCRIPTION",
sc = nil or TERM_SPECIFIC_SCRIPT_OBJECT,
q = nil or {"LEFT_QUALIFIER", "LEFT_QUALIFIER", ...},
qq = nil or {"RIGHT_QUALIFIER", "RIGHT_QUALIFIER", ...},
refs = nil or {{text = "REF_TEXT" or "", name = nil or "REF_NAME", group = nil or "REF_GROUP"}, ...},
separator = nil or "SEPARATOR",
} or "HEAD",
...,
},
genders = {
"GENDER/NUMBER-SPEC" or {spec = "GENDER/NUMBER-SPEC", qualifiers = {"QUALIFIER", "QUALIFIER", ...}},
...
},
inflections = {
enable_auto_translit = BOOLEAN,
{
label = "GRAMMATICAL_CATEGORY",
accel = nil or {
form = "TAG|TAG",
lemma = nil or "LÉMMA",
lemma_translit = nil or "MANUAL_TRANSLIT",
...
},
sc = nil or INFLECTION_SPECIFIC_SCRIPT_OBJECT,
enable_auto_translit = BOOLEAN,
{
term = "INFLECTED_FORM",
alt = nil or "DISPLAY_TEXT",
translit = nil or "MANUAL_TRANSLIT",
transcription = nil or "MANUAL_TRANSCRIPTION",
genders = {
"GENDER/NUMBER-SPEC" or {spec = "GENDER/NUMBER-SPEC", qualifiers = {"QUALIFIER", "QUALIFIER", ...}},
...
},
accel = nil or {
form = "TAG|TAG|TAG",
lemma = nil or "LÉMMA",
lemma_translit = nil or "MANUAL_TRANSLIT",
...
},
lang = nil or TERM_SPECIFIC_LANG_OBJECT,
sc = nil or TERM_SPECIFIC_SCRIPT_OBJECT,
id = "SENSE_ID",
q = nil or {"LEFT_QUALIFIER", "LEFT_QUALIFIER", ...},
qq = nil or {"RIGHT_QUALIFIER", "RIGHT_QUALIFIER", ...},
refs = nil or {{text = "REF_TEXT" or "", name = nil or "REF_NAME", group = nil or "REF_GROUP"}, ...},
separator = nil or "SEPARATOR",
nolink = BOOLEAN,
hypothetical = BOOLEAN,
} or "INFLECTED_FORM" or {
label = "TEXTUAL_LABEL",
q = nil or {"LEFT_QUALIFIER", "LEFT_QUALIFIER", ...},
qq = nil or {"RIGHT_QUALIFIER", "RIGHT_QUALIFIER", ...},
refs = nil or {{text = "REF_TEXT" or "", name = nil or "REF_NAME", group = nil or "REF_GROUP"}, ...},
separator = nil or "SEPARATOR",
},
...,
},
{
label = "GRAMMATICAL_CATEGORY",
request = true,
},
...
},
pos_category = "PLURAL_PART_OF_SPEECH",
categories = {"CATEGORY", "CATEGORY", ...},
translits = nil or {"MANUAL_TRANSLIT", nil, "MANUAL_TRANSLIT", ...},
transcriptions = nil or {nil, "MANUAL_TRANSCRIPTION", "MANUAL_TRANSCRIPTION", ...},
sort_key = nil or "SORT_KEY",
id = nil or "SENSE_ID",
force_cat_output = BOOLEAN,
sccat = BOOLEAN,
noposcat = BOOLEAN,
nogendercat = BOOLEAN,
nomultiwordcat = BOOLEAN,
nopalindromecat = BOOLEAN,
nolink = BOOLEAN,
}
]=]

function export.full_headword(data)

------------ 1. Basic checks for old-style (multi-arg) calling convention. ------------

if data.getCanonicalName then
error("In full_headword(), the first argument `data` needs to be a Lua object (table) of properties, not a language object")
end

if not data.lang or type(data.lang) ~= "table" or not data.lang.getCode then
error("In full_headword(), the first argument `data` needs to be a Lua object (table) and `data.lang` must be a language object")
end

if data.id and type(data.id) ~= "string" then
error("The id in the data table should be a string.")
end

------------ 2. Initialize pagename etc. ------------

local langcode = data.lang:getCode()
local langname = data.lang:getCanonicalName()
local langname = data.lang:getCanonicalName()


if data.pagename then -- for testing, doc pages, etc.
-- Check the namespace against the language type
data.title = mw.title.new(data.pagename)
if not data.title then
error(("Bad value for `data.pagename`: '%s'"):format(data.pagename))
end
else
data.title = mw.title.getCurrentTitle()
end

local pagename = data.title.text
local namespace = data.title.nsText

-- Check the namespace against the language type.
if namespace == "" then
if namespace == "" then
local langtype = data.lang:getType()
local langtype = data.lang:getType()
Line 517: Line 566:
end
end


------------ 3. Initialize `data.heads` table; if old-style, convert to new-style. ------------
local tracking_categories = {}


if not data.noposcat then
if type(data.heads) == "table" and type(data.heads[1]) == "table" then
-- new-style
if data.translits or data.transcriptions then
error("In full_headword(), if `data.heads` is new-style (array of head objects), `data.translits` and `data.transcriptions` cannot be given")
end
else
-- convert old-style `heads`, `translits` and `transcriptions` to new-style
local maxind = math.max(
init_and_find_maximum_index(data, "heads"),
init_and_find_maximum_index(data, "translits", true),
init_and_find_maximum_index(data, "transcriptions", true)
)
for i = 1, maxind do
data.heads[i] = {
term = data.heads[i],
tr = data.translits[i],
ts = data.transcriptions[i],
}
end
end

-- Make sure there's at least one head.
if not data.heads[1] then
data.heads[1] = {}
end

------------ 4. Initialize and validate `data.categories`, determine `pos_category` if not given, and add basic categories. ------------

init_and_find_maximum_index(data, "categories")
local pos_category_already_present = false
if #data.categories > 0 then
local escaped_langname = require("Module:pattern utilities").pattern_escape(langname)
local matches_lang_pattern = "^" .. escaped_langname .. " "
for _, cat in ipairs(data.categories) do
-- Does the category begin with the language name? If not, tag it with a tracking category.
if not cat:find(matches_lang_pattern) then
-- [[Special:WhatLinksHere/Template:tracking/headword/no lang category]]
-- [[Special:WhatLinksHere/Template:tracking/headword/no lang category/LANGCODE]]
track("no lang category", langcode)
end
end

-- If `pos_category` not given, try to infer it from the first specified category. If this doesn't work, we
-- throw an error below.
if not data.pos_category and data.categories[1]:find(matches_lang_pattern) then
data.pos_category = data.categories[1]:gsub(matches_lang_pattern, "")
-- Optimization to avoid inserting category already present.
pos_category_already_present = true
end
end

if not data.pos_category then
error("`data.pos_category` not specified and could not be inferred from the categories given in "
.. "`data.categories`. Either specify the plural part of speech in `data.pos_category` "
.. "(e.g. \"proper nouns\") or ensure that the first category in `data.categories` is formed from the "
.. "language's canonical name plus the plural part of speech (e.g. \"Norwegian Bokmål proper nouns\")."
)
end

-- Insert a category at the beginning for the part of speech unless it's already present or `data.noposcat` given.
if not pos_category_already_present and not data.noposcat then
local pos_category = langname .. " " .. data.pos_category
local pos_category = langname .. " " .. data.pos_category
-- FIXME: [[User:Theknightwho]] Why is this special case here? Please add an explanatory comment.
-- FIXME: [[User:Theknightwho]] Why is this special case here? Please add an explanatory comment.
Line 527: Line 636:
end
end


-- Try to determine whether the part of speech refers to a lemma or a non-lemma form; if we can figure this out,
if data.sccat and data.sc then
-- add an appropriate category.
for i, script in ipairs(data.sc) do
table.insert(data.categories, langname .. " " .. data.pos_category
.. " in " .. script:getDisplayForm())
end
end

-- Is it a lemma category?
local postype = export.pos_lemma_or_nonlemma(data.pos_category)
local postype = export.pos_lemma_or_nonlemma(data.pos_category)
if not postype then
if not postype then
-- We don't know what this category is, so tag it with a tracking category.
-- We don't know what this category is, so tag it with a tracking category.
-- [[Special:WhatLinksHere/Template:tracking/headword/unrecognized pos]]
--[=[
[[Special:WhatLinksHere/Template:tracking/headword/unrecognized pos]]
-- [[Special:WhatLinksHere/Template:tracking/headword/unrecognized pos/LANGCODE]]
track("unrecognized pos", langcode)
]=]
table.insert(tracking_categories, "head tracking/unrecognized pos")
-- [[Special:WhatLinksHere/Template:tracking/headword/unrecognized pos/POS]]
-- [[Special:WhatLinksHere/Template:tracking/headword/unrecognized pos/POS/LANGCODE]]
track("unrecognized pos", data.lang:getCode())
track("unrecognized pos/pos/" .. data.pos_category, data.lang:getCode())
track("unrecognized pos/pos/" .. data.pos_category, langcode)
elseif not data.noposcat then
elseif not data.noposcat then
table.insert(data.categories, 1, langname .. " " .. postype .. "s")
table.insert(data.categories, 1, langname .. " " .. postype .. "s")
end
end


------------ 5. Create a default headword, and add links to multiword page names. ------------
-- Preprocess
preprocess(data, postype)


if namespace == "" and data.lang:getType() ~= "reconstructed" then
-- Determine if term is reconstructed
local is_reconstructed = data.lang:getType() == "reconstructed" or namespace == "Reconstruction"
local m_links = require("Module:links")
for i, head in ipairs(data.heads) do
if title.prefixedText ~= m_links.getLinkPage(m_links.remove_links(head), data.lang, data.sc[i]) then
--[=[
[[Special:WhatLinksHere/Template:tracking/headword/pagename spelling mismatch]]
]=]
track("pagename spelling mismatch", data.lang:getCode())
break
end
end
end


-- Create a default headword.
-- Format and return all the gathered information
local default_head = data.title.subpageText:gsub("^Unsupported titles/", "")
return
format_headword(data) ..
format_genders(data) ..
format_inflections(data) ..
require("Module:utilities").format_categories(
tracking_categories, data.lang, data.sort_key, nil,
data.force_cat_output or test_force_categories, data.sc[1]
)
end


local unmodified_default_head = default_head
function export.full_headword(data)
local tracking_categories = {}


-- Add links to multi-word page names when appropriate
local pagename = title.text
if not m_data.no_multiword_links[langcode] and not is_reconstructed and export.head_is_multiword(default_head) then
local fullPagename = title.fullText
default_head = export.add_multiword_links(default_head)
local namespace = title.nsText

if data.getCanonicalName then
error("In full_headword(), the first argument `data` needs to be a Lua object (table) of properties, not a language object")
end
end


if is_reconstructed then
if not data.lang or type(data.lang) ~= "table" or not data.lang.getCode then
default_head = "*" .. default_head
error("In full_headword(), the first argument `data` needs to be a Lua object (table) and `data.lang` must be a language object")
end
end

------------ 6. Fill in missing values in `data.heads`. ------------
if not data.heads then

-- This is the same practice as the main headword template.
-- True if any script among the headword scripts has spaces in it.
track("heads-is-nil")
local any_script_has_spaces = false
data.heads = {}
-- True if any term has a redundant head= param.
elseif type(data.heads) ~= "table" then
local has_redundant_head_param = false
error("In full_headword(), `data.heads` must be an array of heads but is a " .. type(data.heads))

elseif data.heads[1] == "" then
for _, head in ipairs(data.heads) do
-- This practice is nasty and needs to be eradicated.

track("heads-is-blank-string")
------ 6a. If missing head, replace with default head.
if #data.heads > 1 then
if not head.term then
track("heads-is-blank-string-and-multiple")
head.term = default_head
elseif head.term == default_head then
has_redundant_head_param = true
end
end
end


-- Try to detect the script(s) if not provided.
------ 6b. Try to detect the script(s) if not provided. If a per-head script is provided, that takes precedence,
------ otherwise fall back to the overall script if given. If neither given, autodetect the script.
if data.sc then

if data.sc._type == "script object" then
data.sc = {data.sc}
if head.sc or data.sc then
-- Only create this closure when needed.
local function check_redundant_sc()
if data.noscripttracking then
return
end
-- [[Special:WhatLinksHere/Template:tracking/headword/sc]]
track("sc")

local auto_sc = data.lang:findBestScript(head.term)
local manual_code = head.sc:getCode()
if manual_code == auto_sc:getCode() then
-- [[Special:WhatLinksHere/Template:tracking/headword/sc/redundant]]
-- [[Special:WhatLinksHere/Template:tracking/headword/sc/redundant/SCRIPTCODE]]
track("sc/redundant", manual_code)
else
-- [[Special:WhatLinksHere/Template:tracking/headword/sc/needed]]
-- [[Special:WhatLinksHere/Template:tracking/headword/sc/needed/SCRIPTCODE]]
track("sc/needed", manual_code)
end
end
if head.sc then
-- Per-head script given.
check_redundant_sc()
else
-- Overall script given.
head.sc = data.sc
-- Only check for redundant script if there's a single head present. If multiple heads present, it
-- gets complicated (might be redundant for one but not the other), so don't try.
if #data.heads == 1 then
check_redundant_sc()
end
end
else
else
head.sc = data.lang:findBestScript(head.term)
error("No support now or ever for passing in an array of script objects in `data.sc`; this will be supported using headword objects in `data.heads`")
end
end

end
-- If using a discouraged character sequence, add to maintenance category.
if head.sc:hasNormalizationFixes() == true then
-- FIXME! [[User:Theknightwho]] Checking for #data.sc ~= #data.heads and silently overwriting it is bad software
local composed_head = unfc(head.term)
-- practice. I will be ripping out and rewriting this code. --ben
if head.sc:fixDiscouragedSequences(composed_head) ~= composed_head then
if (not data.sc) or #data.sc == 0 or (#data.sc ~= #data.heads and (#data.sc > 1 or #data.heads > 1)) then
table.insert(data.categories, "Pages using discouraged character sequences")
data.sc = {}
if #data.heads > 1 then
for i, head in ipairs(data.heads) do
data.sc[i] = data.lang:findBestScript(data.heads[i])
end
end
else
data.sc[1] = data.lang:findBestScript(data.heads[1] ~= "" and data.heads[1] or pagename)
end
end
else
-- Track uses of sc parameter
local best = data.lang:findBestScript(pagename)
track("sc")


any_script_has_spaces = any_script_has_spaces or head.sc:hasSpaces()
if data.sc[1]:getCode() == best:getCode() then

track("sc/redundant", data.sc[1]:getCode())
------ 6c. Create automatic transliterations for any non-Latin headwords without manual translit given
else
------ (provided automatic translit is available, e.g. not in Persian or Hebrew).
track("sc/needed", data.sc[1]:getCode())

-- Make transliterations
head.tr_manual = nil

-- Try to generate a transliteration if necessary
if head.tr == "-" then
head.tr = nil
elseif head.tr then
head.tr_manual = true
elseif not notranslit[langcode] and head.sc:isTransliterated() then
head.tr = (data.lang:transliterate(get_module("links").remove_links(head.term), head.sc))
head.tr = head.tr and mw.text.trim(head.tr)

-- There is still no transliteration?
-- Add the entry to a cleanup category.
if not head.tr then
head.tr = "<small>transliteration needed</small>"
table.insert(data.categories, "Requests for transliteration of " .. langname .. " terms")
end

if head.tr then
head.tr_manual = false
end
end

-- Link to the transliteration entry for languages that require this.
if head.tr and data.lang:link_tr() then
head.tr = get_module("links").full_link {
term = head.tr,
lang = data.lang,
sc = get_module("scripts").getByCode("Latn"),
tr = "-"
}
end
end
end
end

------------ 7. Maybe tag the title with the appropriate script code, using the `display_title` mechanism. ------------


-- Assumes that the scripts in "toBeTagged" will never occur in the Reconstruction namespace.
-- Assumes that the scripts in "toBeTagged" will never occur in the Reconstruction namespace.
-- (FIXME: Don't make assumptions like this, and if you need to do so, throw an error if the assumption is violated.)
-- (FIXME: Don't make assumptions like this, and if you need to do so, throw an error if the assumption is violated.)
-- Avoid tagging ASCII as Hani even when it is tagged as Hani in the headword, as in [[check]]. The check for ASCII
-- Avoid tagging ASCII as Hani even when it is tagged as Hani in the headword, as in [[check]]. The check for ASCII
-- might need to be expanded to a check for any Latin characters and whitespace or punctuation. Where there are
-- might need to be expanded to a check for any Latin characters and whitespace or punctuation.
local display_title
-- multiple headwords, use the script for the first. (FIXME: [[User:Theknightwho]] Why are we using the script for the
-- Where there are multiple headwords, use the script for the first. This assumes the first headword is similar to
-- first headword for all of them rather than the correct per-headword script? Please either expand this comment
-- the pagename, and that headwords that are in different scripts from the pagename aren't first. This seems to be
-- explaining why, or correct it if it's a problem.)
-- about the best we can do (alternatively we could potentially do script detection on the pagename).
local displayTitle
local dt_script = data.heads[1].sc
if (namespace == "" and data.sc and toBeTagged[data.sc[1]:getCode()]
local dt_script_code = dt_script:getCode()
and not pagename:find "^[%z\1-\127]+$")
local page_non_ascii = namespace == "" and not pagename:find("^[%z\1-\127]+$")
or (data.sc[1]:getCode() == "Jpan" and (test_script(pagename, "Hira") or test_script(pagename, "Kana")))
if page_non_ascii and toBeTagged[dt_script_code]
or (data.sc[1]:getCode() == "Kore" and (test_script(pagename, "Hang"))) then
or (dt_script_code == "Jpan" and (text_in_script(pagename, "Hira") or text_in_script(pagename, "Kana")))
displayTitle = '<span class="' .. data.sc[1]:getCode() .. '">' .. pagename .. '</span>'
or (dt_script_code == "Kore" and text_in_script(pagename, "Hang")) then
display_title = '<span class="' .. dt_script_code .. '">' .. pagename .. '</span>'
-- Keep Han entries region-neutral in the display title.
-- Keep Han entries region-neutral in the display title.
elseif page_non_ascii and (dt_script_code == "Hant" or dt_script_code == "Hans") then
elseif namespace == "" and data.sc and not pagename:find "^[%z\1-\127]+$"
display_title = '<span class="Hani">' .. pagename .. '</span>'
and (data.sc[1]:getCode() == "Hant" or data.sc[1]:getCode() == "Hans") then
displayTitle = '<span class="Hani">' .. pagename .. '</span>'
elseif namespace == "Reconstruction" then
elseif namespace == "Reconstruction" then
displayTitle, matched = rsubn(
display_title, matched = rsubn(
data.title.fullText,
fullPagename,
"^(Reconstruction:[^/]+/)(.+)$",
"^(Reconstruction:[^/]+/)(.+)$",
function(before, term)
function(before, term)
Line 662: Line 803:
term,
term,
data.lang,
data.lang,
data.sc[1]
dt_script
)
)
end
end
Line 668: Line 809:


if matched == 0 then
if matched == 0 then
displayTitle = nil
display_title = nil
end
end
end
end


if displayTitle then
if display_title then
local frame = mw.getCurrentFrame()
local frame = mw.getCurrentFrame()
frame:callParserFunction(
frame:callParserFunction(
"DISPLAYTITLE",
"DISPLAYTITLE",
display_title
displayTitle
)
)
end
end

------------ 8. Insert additional categories. ------------


if data.force_cat_output then
if data.force_cat_output then
-- [[Special:WhatLinksHere/Template:tracking/headword/force cat output]]
--[=[
[[Special:WhatLinksHere/Template:tracking/headword/force cat output]]
]=]
track("force cat output")
track("force cat output")
end
end


if has_redundant_head_param then
-- Were any categories specified?
track("redundant-head-param", langcode)
if data.categories and #data.categories > 0 then
if not data.no_redundant_head_cat and langcode == "en" then
local lang_name = require("Module:pattern utilities").pattern_escape(data.lang:getCanonicalName())
for _, cat in ipairs(data.categories) do
table.insert(data.categories, langname .. " terms with redundant head parameter")
-- Does the category begin with the language name? If not, tag it with a tracking category.
if not rfind(cat, "^" .. lang_name) then
mw.log(cat, data.lang:getCanonicalName())
table.insert(tracking_categories, "head tracking/no lang category")

--[=[
[[Special:WhatLinksHere/Template:tracking/head tracking/no lang category]]
]=]
track("no lang category", data.lang:getCode())
end
end
end
end


-- If the first head is multiword (after removing links), maybe insert into "LANG multiword terms".
if not data.pos_category
if not data.nomultiwordcat and any_script_has_spaces and postype == "lemma" and not m_data.no_multiword_cat[langcode] then
and data.categories[1]:find("^" .. data.lang:getCanonicalName())
-- Check for spaces or hyphens, but exclude prefixes and suffixes.
then
-- Use the pagename, not the head= value, because the latter may have extra
data.pos_category = data.categories[1]:gsub("^" .. data.lang:getCanonicalName() .. " ", "")
-- junk in it, e.g. superscripted text that throws off the algorithm.
table.remove(data.categories, 1)
local checkpattern = ".[%s%-፡]."
if m_data.hyphen_not_multiword_sep[langcode] then
-- Exclude hyphens if the data module states that they should for this language
checkpattern = ".[%s፡]."
end
if rfind(unmodified_default_head, checkpattern) and not non_categorizable(data) then
table.insert(data.categories, langname .. " multiword terms")
end
end
end
end


if not data.pos_category then
if data.sccat then
for _, head in ipairs(data.heads) do
error(
table.insert(data.categories, langname .. " " .. data.pos_category .. " in " .. head.sc:getDisplayForm())
'No valid part-of-speech categories were found in the list '
end
.. 'of categories passed to the function "full_headword". '
.. 'The part-of-speech category should consist of a language\'s '
.. 'canonical name plus a part of speech.'
)
end
end


Line 724: Line 860:


if standard then
if standard then
if ulen(title.subpageText) ~= 1 and not non_categorizable() then
if ulen(data.title.subpageText) ~= 1 and not non_categorizable(data) then
for character in rgmatch(title.subpageText, "([^" .. standard .. "])") do
for character in rgmatch(data.title.subpageText, "([^" .. standard .. "])") do
local upper = uupper(character)
local upper = uupper(character)
if not rfind(upper, "[" .. standard .. "]") then
if not rfind(upper, "[" .. standard .. "]") then
character = upper
character = upper
end
end
table.insert(
table.insert(data.categories, langname .. " terms spelled with " .. character)
data.categories,
data.lang:getCanonicalName() .. " terms spelled with " .. character
)
end
end
end
end
Line 739: Line 872:


-- Categorise for palindromes
-- Categorise for palindromes
if not data.nopalindromecat and title.nsText ~= "Reconstruction" and ulen(title.subpageText) > 2
if not data.nopalindromecat and namespace ~= "Reconstruction" and ulen(data.title.subpageText) > 2
-- FIXME: Use of first script here seems hacky. What is the clean way of doing this in the presence of
and require("Module:palindromes").is_palindrome(
-- multiple scripts?
title.subpageText, data.lang, data.sc[1]
and require("Module:palindromes").is_palindrome(data.title.subpageText, data.lang, data.heads[1].sc) then
) then
table.insert(data.categories, data.lang:getCanonicalName() .. " palindromes")
table.insert(data.categories, langname .. " palindromes")
end
end


if namespace == "" and data.lang:getType() ~= "reconstructed" then
-- This may add more categories (e.g. gender categories), so make sure it gets
local m_links = get_module("links")
-- evaluated first.
for _, head in ipairs(data.heads) do
local text = show_headword_line(data)
if data.title.prefixedText ~= m_links.getLinkPage(m_links.remove_links(head.term), data.lang, head.sc) then
-- [[Special:WhatLinksHere/Template:tracking/headword/pagename spelling mismatch]]
-- [[Special:WhatLinksHere/Template:tracking/headword/pagename spelling mismatch/LANGCODE]]
track("pagename spelling mismatch", data.lang:getCode())
break
end
end
end

------------ 9. Format and return headwords, genders, inflections and categories. ------------

-- Format and return all the gathered information. This may add more categories (e.g. gender/number categories),
-- so make sure we do it before evaluating `data.categories`.
local text =
format_headword(data) ..
format_genders(data) ..
format_inflections(data)

return
return
text ..
text ..
require("Module:utilities").format_categories(
require("Module:utilities").format_categories(
data.categories, data.lang, data.sort_key, nil,
data.categories, data.lang, data.sort_key, nil,
data.force_cat_output or test_force_categories, data.sc[1]
data.force_cat_output or test_force_categories, data.heads[1].sc
) ..
)
require("Module:utilities").format_categories(
tracking_categories, data.lang, data.sort_key, nil,
data.force_cat_output or test_force_categories, data.sc[1]
)
end
end



Revision as of 08:27, 8 March 2023

This module is used to show headword lines, along with any annotations like genders, transliterations and inflections. It's used by the template {{head}}, via the submodule Module:headword/templates. It's also used by many other headword modules; for a full list, see Category:Headword-line modules. Some of the data used by this module is found in Module:headword/data.

export.head_is_multiword

function export.head_is_multiword(head)

This function lacks documentation. Please add a description of its usages, inputs and outputs, or its difference from similar functions, or make it local to remove it from the function list.

function export.add_multiword_links(head)

This function lacks documentation. Please add a description of its usages, inputs and outputs, or its difference from similar functions, or make it local to remove it from the function list.

export.pos_lemma_or_nonlemma

function export.pos_lemma_or_nonlemma(plpos, best_guess)

This function lacks documentation. Please add a description of its usages, inputs and outputs, or its difference from similar functions, or make it local to remove it from the function list.

export.full_headword

function export.full_headword(data)

This function lacks documentation. Please add a description of its usages, inputs and outputs, or its difference from similar functions, or make it local to remove it from the function list.

Further explanations for full_headword()

The sole argument, data, is a table containing the following items (WARNING: they will be destructively modified):

{
	lang = language_object,
	pagename = nil or "pagename",
	heads = { "head1", "head2", "head3", ... } or {
		{
			term = nil or "head1",
			tr = nil or "translit1",
			ts = nil or "transcription1",
			sc = nil or script_object,
			q = nil or {"left_qualifier1", "left_qualifier2", ...},
			qq = nil or {"right_qualifier1", "right_qualifier2", ...},
			refs = nil or {{text = "ref_text1" or "", name = nil or "ref_name1", group = nil or "ref_group1"}, ...},
			separator = nil or "separator",
		},
		...
	},
	translits = { [1] = "translit1", [3] = "translit3", ... },
	transcriptions = { [2] = "transcription2", [3] = "transcription3", ... },
	sc = script_object,
	inflections = {
		enable_auto_translit = boolean,
		{ label = "grammatical_category", "inflected_form1", "inflected_form2", ... },
		{ label = "grammatical_category", accel = {form = "tag|tag", lemma = "lémma"}, "inflected_form1", "inflected_form2", ... },
		{
			label = "grammatical_category",
			accel = {
				form = "tag|tag",
				target = "form_target",
				tr = nil or "form_manual_translit",
				gender = "gender_spec" or {"gender_spec1", "gender_spec2", ...},
				pos = "form_part_of_speech",
				lemma = nil or "lémma",
				lemma_translit = nil or "lemma_manual_translit",
				no_store = boolean,
			},
			sc = nil or inflection_specific_script_object,
			enable_auto_translit = boolean,
			"inflected_form1",
			{
				term = "inflected_form2",
				alt = nil or "display_text",
				translit = nil or "manual_transliteration",
				transcription = nil or "manual_transcription",
				gender = {"gender1", "gender2", {spec = "gender3", qualifiers = nil or {"qualifier1", "qualifier2", ... }}},
				accel = {form = "tag|tag|tag", lemma = "lemma_of_inflected_form", lemma_translit = "manual_translit" },
				lang = nil or term_specific_lang_object,
				sc = nil or term_specific_script_object,
				id = "sense_id",
				q = nil or {"left_qualifier1", "left_qualifier2", ... },
				qq = nil or {"right_qualifier1", "right_qualifier2", ... },
				refs = nil or {{text = "ref_text1" or "", name = nil or "ref_name1", group = nil or "ref_group1"}, ...},
				separator = nil or "separator",
				nolinkinfl = boolean,
				face = nil or "plain" or "hypothetical",
			},
			{
				label = "raw_textual_label",
				q = nil or {"left_qualifier1", "left_qualifier2", ... },
				qq = nil or {"right_qualifier1", "right_qualifier2", ... },
				refs = nil or {{text = "ref_text1" or "", name = nil or "ref_name1", group = nil or "ref_group1"}, ...},
				separator = nil or "separator",
			},
			...
		},
		{ label = "grammatical_category", request = true },
		...
	},
	genders = {
		"gender1",
		{spec = "gender2", qualifiers = {"qualifier1", "qualifier2", ...}},
		...
	},
	pos_category = "plural_part_of_speech",
	categories = { "category1", "category2", ... },
	whole_page_categories = { "category1", "category2", ... },
	force_cat_output = boolean,
	sccat = boolean,
	noposcat = boolean,
	nogendercat = boolean,
	nomultiwordcat = boolean,
	nopalindromecat = boolean,
	nolinkhead = boolean,
	nolinkinfl = boolean,
	sort_key = "sort_key",
	id = "sense_id",
}

Further explanation:

  • data.lang is required and is a language object from Module:languages corresponding to a given language. For example, use require("Module:languages").getByCode("ru") to retrieve the object corresponding to Russian.
  • data.pagename is optional and allows you to override the pagename used variously in the module (e.g. as the default value when a head is omitted, for setting categories such as palindromes and terms spelled with CHAR, etc.).
  • data.heads is a table listing the heads of the headword. Each element is either a string specifying only the headword itself (old-style), or an object specifying all the properties of the headword (new-style). You cannot mix and match these two styles; all elements should be of one type or the other. If no heads are specified at all (data.heads is omitted or is an empty array), a default head is set based on the assumed pagename (either the actual pagename or the value of data.pagename, if set). When using old-style head strings, a given head in the array can be nil, in which case a default head is set as above. When head objects are used, a given object can have the following properties:
    • .term: A string specifying the headword. This can be omitted, in which case a default head is set as above. Explicit headwords are generally used to specify extra diacritics (in languages with such diacritics, e.g. Russian, Arabic, Latin, Ancient Greek, Old English, etc.), or to link individual words of a multiword term, particularly when the words are inflected forms. Note that by default, each word is linked individually to itself, so there is no need to specify links for a term like a golden key can open any door. Some additional notes:
      • If a headword string contains wikilinks, they are converted into language-section links for the given language (using Module:links#language_link, which is also used by {{l}}). For example, giving "[[give]] [[up]]", if the language provided is English, will produce: "[[give#English|give]] [[up#English|up]]". If string is prefixed with * or if any of the links are, then they are interpreted as reconstructed terms and it will create links to the Reconstruction namespace as appropriate.
      • If the page name contains spaces or punctuation marks (except for punctuation marks that are used inside of words), it is split and each individual word is automatically wikilinked as above.
      • If the current page is in the Reconstruction: namespace, then an asterisk "*" will be prepended to the headword to indicate that it is a reconstructed term.
    • .tr: A string specifying the transliteration of the headword. This is only needed when the headword is in a non-Latin script, and even then only when the automatic transliteration specified using the language's transliteration module is incorrect (or the language has no transliteration module, such as with Persian and Hebrew). For languages with a transliteration module, pass in "-" to suppress the transliteration entirely.
    • .ts: A string specifying the transcription of the headword. This is only used in a few languages with non-Latin scripts where the spelling is significantly different from the pronunciation, such as Akkadian, Old Persian or Hittite. In cases like this, the transliteration usually reflects the spelling and the transcription reflects the pronunciation. For this reason, transcriptions are displayed between slashes. Transcriptions should NOT be used simply to display IPA pronunciation of a language like Russian or Arabic. Unlike for transliterations, there are no automatic transcription modules.
    • .sc: An optional script object from Module:scripts corresponding to a given script, specifying the script that the headword is in. If omitted, defaults to the top-level data.sc value. Most of the time, neither the per-headword script nor top-level script need to be specified: If both are omitted, Module:scripts will determine the script(s) using the list of scripts in the language's data file and the characters that are in the headword. Specifically, if there are multiple possible scripts for a language, the script with the largest number of characters in the headword is chosen.
    • .q: An optional array specifying one of more qualifiers displayed to the left of the headword. Qualifiers are displayed in italics and with parentheses around them, and are intended to specify relevant properties of the headword, especially when there is more than one headword.
    • .qq: An optional array specifying one of more qualifiers displayed to the right of the headword, as above.
    • .refs: An optional array specifying one of more references (i.e. footnotes) for the headword. This is similar to using <ref>...</ref> to specify a reference/footnote after a given word in the text. Each element of the array is either a string (the text of the reference) or an object of the form {text = "ref_text" or "", name = nil or "ref_name", group = nil or "ref_group"}. In this latter format:
      • .text specifies the reference text (which cannot be nil; use a blank string when cross-referencing to another reference);
      • .name gives an optional name to the reference for cross-reference purposes, if the reference text is non-empty, similarly to <ref name="ref_name">ref_text</ref>; however, if the reference text is empty, it specifies a cross-reference to a previously-named reference, similarly to <ref name="ref_name"/>;
      • .group gives an optional group to the reference for grouping purposes, similarly to <ref name="ref_name" group="ref_group">ref_text</ref>; however, if the reference text is empty, it specifies the group of a cross-reference to a previously-named reference, similarly to <ref name="ref_name" group="ref_group"/>.
    • .separator: The separator preceding the headword. If omitted, the default value is  <i>or</i>  (i.e. the italicized word or surrounded by spaces) for the second and higher headword, and a blank string for the first headword. Use a blank string to request no separator at all.
  • data.translits is an optional table listing the transliterations corresponding to each headword in data.heads, when old-style head strings are used; omitting this field is equivalent to setting it to an empty list. If new-style head objects are used, this field must be omitted. The Nth numbered entry should be either a string specifying the transliteration of headword N, or may be omitted, as with the .tr property described above. Note that, if there are multiple headwords, the table in data.translits might have entries in the middle of the list that are nil. A list of this sort cannot be created with table.insert(), as attempting to insert nil this way does nothing. Instead, each transliteration must be explicitly assigned using a number as index, e.g. { [1] = "string", [3] = "string", ... }; here, item 2 is nil, because no value was assigned to it.
  • data.transcriptions is an optional table listing the transcriptions corresponding to each headword in data.heads, when old-style head strings are used; omitting this field is equivalent to setting it to an empty list. If new-style head objects are used, this field must be omitted. It is of the same format as data.translits, and can have holes in it as needed. The meaning of the transcription field is as described abobe for .ts.
  • data.sc is an optional script object from Module:scripts corresponding to a given script. If specified, this applies equally to all heads specified using data.heads; if you need to specify per-head scripts, use the head object format documented above. Most of the time you can omit this item, and Module:scripts will determine the script(s) as specified above for the .sc headword property.
  • data.genders is a table listing the gender/number specifications for the headwords. This can be omitted for no genders or numbers. Each element is either a string specifying a gender/number spec, or a table of the form {spec = "gender/number_spec", qualifiers = nil or {"qualifier1", "qualifier2", ...}. In either case, the accepted values for genders or numbers are given in Module:gender and number; examples are "m" for masculine, "f-an-p" for feminine animate plural and "c2" for noun class 2 in languages such as Swahili that have noun classes. If the format with qualifiers is given, the qualifiers are displayed to the left of the gender/number specification. Categories are automatically added according to the specific genders, e.g. LANG masculine nouns for the language specified in data.lang if the gender is masculine and the part of speech (see below) is nouns or reconstructed nouns. To suppress the addition of these categories, specify data.nogendercat = true.
  • data.inflections is a table listing the inflections to be displayed in the headword entry. The format of this table is somewhat complex and is described below under format_inflections.
  • data.pos_category is the part-of-speech category for the entry. This is one of the lemma and nonlemma parts of speech listed in Module:headword/data. It should be in the plural: for example, "nouns". If this item is omitted, the part of speech category must be included in as the first item in data.categories.
  • data.categories is a table listing the categories to which the entry containing the headword will be added. The first category should be a part-of-speech category, with the canonical name of the language at the beginning – "Russian nouns" – unless the part of speech is given in the field data.pos_category.
  • data.whole_page_categories is a table listing language-agnostic categories to which the page will be added, which it is nevertheless useful for the headword module to handle (e.g. Category:Unsupported titles). Because they are not tied to a language, pages in them should be sorted according to their {{DEFAULTSORT:}} values for the sake of consistency. Note that some of these - including "Category:Unsupported titles" - are already handled automatically.
  • data.sort_key is a string specifying a sort key for the categories listed in data.categories. Sort keys should usually be omitted, because the format_categories function in Module:utilities will generate a suitable sortkey in most cases. The sortkey is used to ensure that the page is listed in the correct order in the categories to which it belongs.
  • data.nolinkhead is a boolean value determining whether or not to link the individual words of a multiword headword, which is done by default.
  • data.nolinkinfl is a boolean value determining whether or not to link the inflections of the entire headword. Not to be confused with part.nolinkinfl, which disables linking only for one of the inflections. It is used, for example, by Module:la-headword for reconstructed terms.

Examples

A simple example

full_headword{
	lang = require("Module:languages").getByCode("en"),	-- language code
	heads = {"book"},									-- headwords
	inflections = {
		{label = "plural", "books"}						-- inflections
	},
	categories = {"English nouns"},						-- part-of-speech category
}

might give (depending on the page it's run on):

<strong class="Latn headword" lang="en">book</strong> (''plural'' <b class="Latn" lang="en">[[books#English|books]]</b>)[[Category:English lemmas|HEADWORD]][[Category:English nouns|HEADWORD]]

which displays as:

book (plural books)

A fuller example

full_headword{
	lang = require("Module:languages").getByCode("de"),
	heads = {"Hund"},
	genders = {"m"},
	inflections = {
		{label = "genitive", "Hundes", "Hunds"},
        {label = "plural", "Hunde", {term="Hünde", q="nonstandard"}},
		{label = "diminutive",
			{term = "Hündchen", genders = {"n"}},
			{nolinkinfl=true, term = "Hündlein", genders = {"n"}}
		}
	},
	categories = {"German nouns"},
}

might give (depending on the page it's run on):

<strong class="Latn headword" lang="de">Hund</strong>&nbsp;<span class="gender"><abbr title="masculine gender">m</abbr></span> (''genitive'' <b class="Latn" lang="de">[[Hundes#German|Hundes]]</b> ''or'' <b class="Latn" lang="de">[[Hunds#German|Hunds]]</b>, ''plural'' <b class="Latn" lang="de">[[Hunde#German|Hunde]] </b>''or (nonstandard)''<b> [[Hünde#German|Hünde]]</b>, ''diminutive'' <b class="Latn" lang="de">[[Hündchen#German|Hündchen]]</b>&nbsp;<span class="gender"><abbr title="neuter gender">n</abbr></span> ''or'' <b class="Latn" lang="de">Hündlein</b>&nbsp;<span class="gender"><abbr title="neuter gender">n</abbr></span>)[[Category:German lemmas|HEADWORD]][[Category:German nouns|HEADWORD]]

which displays as:

Hund m (genitive Hundes or Hunds, plural Hunde or (nonstandard) Hünde, diminutive Hündchen n or Hündlein n)

An example in a non-Latin script

This example is in Russian, which has automatic transliteration:

full_headword{
	lang = require("Module:languages").getByCode("ru"),
	heads = {"кни́га"},
	genders = {"f-in"},
	inflections = {
		{label = "genitive", "кни́ги"},
		{label = "nominative plural", "кни́ги"},
		{label = "genitive plural", "книг"}
	},
	categories = {"Russian nouns"},
}

might give (depending on the page it's run on):

<strong class="Cyrl headword" lang="ru">кни́га</strong> [[Wiktionary:Russian transliteration|•]] (<span class="tr" lang=""><span class="tr" lang="">kníga</span></span>)&nbsp;<span class="gender"><abbr title="feminine gender">f</abbr>&nbsp;<abbr title="inanimate">inan</abbr></span> (''genitive'' <b class="Cyrl" lang="ru">[[книги#Russian|кни́ги]]</b>, ''nominative plural'' <b class="Cyrl" lang="ru">[[книги#Russian|кни́ги]]</b>, ''genitive plural'' <b class="Cyrl" lang="ru">[[книг#Russian|книг]]</b>)[[Category:Russian lemmas|HEADWORD]][[Category:Russian nouns|HEADWORD]]

which displays as

кни́га (knígaf inan (genitive кни́ги, nominative plural кни́ги, genitive plural книг)

Note a few things about the transliteration:

  • If the transliteration is specified and non-empty, Module:headword adds some stuff before and after it. For example, if the transliteration is "foo" and the language is Hebrew, produces
     [[Wiktionary:Hebrew transliteration|•]] (<span lang="">foo</span>)
    
    which looks like “ (foo)”.
  • The bullet linking to a transliteration policy page is only added if the page actually exists.

A fuller example in a non-Latin script

This example is in Russian, with two headwords, each of which requires manual transliteration:

full_headword{
	lang = require("Module:languages").getByCode("ru"),
	heads = {
		{term = "интервьюе́р", tr = "intɛrvʹjuér"},
		{term = "интервью́ер", "intɛrvʹjújer"},
	},
	genders = {"m-an"},
	inflections = {
		{label = "genitive", "интервьюе́ра", "интервью́ера"},
		{label = "nominative plural", "интервьюе́ры", "интервью́еры"},
		{label = "genitive plural", "интервьюе́ров", "интервью́еров"},
	},
	categories = {"Russian nouns"},
}

might give (depending on the page it's run on):

<strong class="Cyrl headword" lang="ru">интервьюе́р</strong> ''or'' <strong class="Cyrl headword" lang="ru">интервью́ер</strong> [[Wiktionary:Russian transliteration|•]] (<span class="tr" lang=""><span class="tr" lang="">intɛrvʹjuér</span> ''or'' <span class="tr" lang="">intɛrvʹjújer</span></span>)&nbsp;<span class="gender"><abbr title="masculine gender">m</abbr>&nbsp;<abbr title="animate">anim</abbr></span> (''genitive'' <b class="Cyrl" lang="ru">[[интервьюера#Russian|интервьюе́ра]]</b> ''or'' <b class="Cyrl" lang="ru">[[интервьюера#Russian|интервью́ера]]</b>, ''nominative plural'' <b class="Cyrl" lang="ru">[[интервьюеры#Russian|интервьюе́ры]]</b> ''or'' <b class="Cyrl" lang="ru">[[интервьюеры#Russian|интервью́еры]]</b>, ''genitive plural'' <b class="Cyrl" lang="ru">[[интервьюеров#Russian|интервьюе́ров]]</b> ''or'' <b class="Cyrl" lang="ru">[[интервьюеров#Russian|интервью́еров]]</b>)[[Category:Russian lemmas|HEADWORD]][[Category:Russian nouns|HEADWORD]]

which displays as

интервьюе́р or интервью́ер (intɛrvʹjuér or intɛrvʹjújerm anim (genitive интервьюе́ра or интервью́ера, nominative plural интервьюе́ры or интервью́еры, genitive plural интервьюе́ров or интервью́еров)

Another fuller example in a non-Latin script

This example is in Arabic, with embedded links in the headword, manual transliteration in an inflection and use of enable_auto_translit:

full_headword{
	lang = require("Module:languages").getByCode("ar"),
	heads = {
		{term = "[[غُدّة]] [[بَصَلِيّ|بَصَلِيّة]] [[إحْلِيلِيّ|إحْلِيلِيّة]]", tr = "ḡudda baṣaliyya ʾiḥlīliyya"},
	},
	genders = {"f"},
	inflections = {
		enable_auto_translit = true,
		{label = "plural", {term="غُدَد بَصَلِيَّة إِحْلِيلِيَة", translit="ḡudad baṣaliyya ʾiḥlīliyya"}},
	},
	categories = {"Arabic nouns"},
}

might give (depending on the page it's run on):

<strong class="Arab headword" lang="ar">[[غدة#Arabic|غُدّة]] [[بصلي#Arabic|بَصَلِيّة]] [[إحليلي#Arabic|إحْلِيلِيّة]]</strong> [[Wiktionary:Arabic transliteration|•]] (<span class="tr" lang=""><span class="tr" lang="">ḡudda baṣaliyya ʾiḥlīliyya</span></span>)&nbsp;<span class="gender"><abbr title="feminine gender">f</abbr></span> (''plural'' <b class="Arab" lang="ar">[[غدد بصلية إحليلية#Arabic|غُدَد بَصَلِيَّة إِحْلِيلِيَة]]</b> (<span lang="" class="tr">ḡudad baṣaliyya ʾiḥlīliyya</span>))[[Category:Arabic lemmas|HEADWORD]][[Category:Arabic nouns|HEADWORD]]

which displays as

غُدّة بَصَلِيّة إحْلِيلِيّة (ḡudda baṣaliyya ʾiḥlīliyyaf (plural غُدَد بَصَلِيَّة إِحْلِيلِيَة (ḡudad baṣaliyya ʾiḥlīliyya))

Proposed/planned changes

  • Checking for invalid genders, given a list of genders that are valid for a particular language.

local export = {}

local rfind = mw.ustring.find
local rgmatch = mw.ustring.gmatch
local rsubn = mw.ustring.gsub
local ulen = mw.ustring.len
local unfc = mw.ustring.toNFC
local uupper = mw.ustring.upper

local m_data = mw.loadData("Module:headword/data")

local isLemma = m_data.lemmas
local isNonLemma = m_data.nonlemmas
local notranslit = m_data.notranslit
local toBeTagged = m_data.toBeTagged

-- If set to true, categories always appear, even in non-mainspace pages
local test_force_categories = false

local modules = {}

local function get_module(mod)
	if not modules[mod] then
		modules[mod] = require("Module:" .. mod)
	end
	return modules[mod]
end

-- version of rsubn() that discards all but the first return value
local function rsub(term, foo, bar)
	local retval = rsubn(term, foo, bar)
	return retval
end

-- Add a tracking category to track entries with certain (unusually undesirable) properties. `track_id` is an identifier
-- for the particular property being tracked and goes into the tracking page. Specifically, this adds a link in the
-- page text to [[Template:tracking/headword/TRACK_ID]], meaning you can find all entries with the `track_id` property
-- by visiting [[Special:WhatLinksHere/Template:tracking/headword/TRACK_ID]].
--
-- If `code` (a language or script code) is given, an additional tracking page
-- [[Template:tracking/headword/TRACK_ID/CODE]] is linked to, and you can find all entries in the combination of
-- `track_id` and `code` by visiting [[Special:WhatLinksHere/Template:tracking/headword/TRACK_ID/CODE]]. This makes it
-- possible to isolate only the entries with a specific tracking property that are in a given language or script.
local function track(track_id, code)
	local tracking_page = "headword/" .. track_id
	local m_debug_track = get_module("debug/track")
	if code then
		m_debug_track{tracking_page, tracking_page .. "/" .. code}
	else
		m_debug_track(tracking_page)
	end
	return true
end


local function text_in_script(text, script_code)
	local sc = get_module("scripts").getByCode(script_code)
	if not sc then
		error("Internal error: Bad script code " .. script_code)
	end
	local characters = sc:getCharacters()

	local out
	if characters then
		text = rsub(text, "%W", "")
		out = rfind(text, "[" .. characters .. "]")
	end

	if out then
		return true
	else
		return false
	end
end


local spacingPunctuation = "[%s%p]+"
--[[ List of punctuation or spacing characters that are found inside of words.
	 Used to exclude characters from the regex above. ]]
local wordPunc = "-־׳״'.·*’་•:"
local notWordPunc = "[^" .. wordPunc .. "]+"


-- Format a term (either a head term or an inflection term) along with any left or right qualifiers, references or
-- customized separator: `part` is the object specifying the term, which should optionally contain:
-- * left qualifiers in `q`, an array of strings (or `qualifiers` for compatibility purposes);
-- * right qualifiers in `qq`, an array of strings;
-- * references in `refs`, an array either of strings (formatted reference text) or objects containing fields `text`
--   (formatted reference text) and optionally `name` and/or `group`;
-- * a separator in `separator`, defaulting to " <i>or</i> " if this is not the first term (j > 1), otherwise "".
-- `formatted` is the formatted version of the term itself, and `j` is the index of the term.
local function format_term_with_qualifiers_and_refs(part, formatted, j)
	local left_qualifiers, right_qualifiers
	local reftext

	left_qualifiers = part.q and #part.q > 0 and part.q or part.qualifiers and #part.qualifiers > 0 and part.qualifiers
	if left_qualifiers then
		left_qualifiers = get_module("qualifier").format_qualifier(left_qualifiers) .. " "
		-- [[Special:WhatLinksHere/Template:tracking/headword/qualifier]]
		track("qualifier")
	end
	if part.qualifiers then
		-- FIXME: Eliminate these usages.
		-- [[Special:WhatLinksHere/Template:tracking/headword/old-qualifier]]
		track("old-qualifier")
	end

	right_qualifiers = part.qq and #part.qq > 0 and part.qq
	if right_qualifiers then
		right_qualifiers = " " .. get_module("qualifier").format_qualifier(right_qualifiers)
		-- [[Special:WhatLinksHere/Template:tracking/headword/qualifier]]
		track("qualifier")
	end
	if part.refs and #part.refs > 0 then
		local refs = {}
		for _, ref in ipairs(part.refs) do
			if type(ref) ~= "table" then
				ref = {text = ref}
			end
			local refargs
			if ref.name or ref.group then
				refargs = {name = ref.name, group = ref.group}
			end
			table.insert(refs, mw.getCurrentFrame():extensionTag("ref", ref.text, refargs))
		end
		reftext = table.concat(refs)
	end

	local separator = part.separator or j > 1 and " <i>or</i> " -- use "" to request no separator

	if left_qualifiers then
		formatted = left_qualifiers .. formatted
	end
	if reftext then
		formatted = formatted .. reftext
	end
	if right_qualifiers then
		formatted = formatted .. right_qualifiers
	end
	if separator then
		formatted = separator .. formatted
	end

	return formatted
end


-- Return true if the given head is multiword according to the algorithm used
-- in full_headword().
function export.head_is_multiword(head)
	for possibleWordBreak in rgmatch(head, spacingPunctuation) do
		if rfind(possibleWordBreak, notWordPunc) then
			return true
		end
	end

	return false
end


-- Add links to a multiword head.
function export.add_multiword_links(head)
	local function workaround_to_exclude_chars(s)
		return rsub(s, notWordPunc, "]]%1[[")
	end

	head = "[[" .. rsub(head, spacingPunctuation, workaround_to_exclude_chars) .. "]]"
	--[=[
	use this when workaround is no longer needed:

	head = "[[" .. rsub(head, WORDBREAKCHARS, "]]%1[[") .. "]]"

	Remove any empty links, which could have been created above
	at the beginning or end of the string.
	]=]
	head = head:gsub("%[%[%]%]", "")
	return head
end


local function non_categorizable(data)
	return (data.title:inNamespace("") and data.title.text:find("^Unsupported titles/"))
		or (data.title:inNamespace("Appendix") and data.title.text:find("^Gestures/"))
end


-- Format a headword with transliterations.
local function format_headword(data)
	local m_scriptutils = require("Module:script utilities")

	-- Are there non-empty transliterations?
	local has_translits = false
	local has_manual_translits = false

	------ Format the headwords. ------

	local head_parts = {}

	for j, head in ipairs(data.heads) do
		if head.tr or head.ts then
			has_translits = true
		end
		if head.tr and head.tr.is_manual or head.ts then
			has_manual_translits = true
		end

		local formatted

		-- Apply processing to the headword, for formatting links and such.
		if head.term:find("[[", nil, true) and head.sc:getCode() ~= "Imag" then
			formatted = get_module("links").language_link({term = head.term, lang = data.lang}, false)
		else
			formatted = data.lang:makeDisplayText(head.term, head.sc, true)
		end

		-- Add language and script wrapper.
		formatted = m_scriptutils.tag_text(formatted, data.lang, head.sc, "head", nil, j == 1 and data.id or nil)

		-- Add qualifiers, references and separator.
		table.insert(head_parts, format_term_with_qualifiers_and_refs(head, formatted, j))
	end

	head_parts = table.concat(head_parts)

	if has_manual_translits then
		-- [[Special:WhatLinksHere/Template:tracking/headword/has-manual-translit]]
		-- [[Special:WhatLinksHere/Template:tracking/headword/has-manual-translit/LANGCODE]]
		track("has-manual-translit", data.lang:getCode())
	end

	------ Format the transliterations and transcriptions. ------

	local translits_formatted

	if has_translits then
		local translit_parts = {}
		for i, head in ipairs(data.heads) do
			if head.tr or head.ts then
				local this_parts = {}
				if head.tr then
					table.insert(this_parts, m_scriptutils.tag_translit(head.tr, data.lang:getCode(), "head", nil, head.tr_manual))
					if head.ts then
						table.insert(this_parts, " ")
					end
				end
				if head.ts then
					table.insert(this_parts, m_scriptutils.tag_transcription(head.ts, data.lang:getCode(), "head"))
				end
				table.insert(translit_parts, table.concat(this_parts))
			end
		end

		translits_formatted = " (" .. table.concat(translit_parts, " <i>or</i> ") .. ")"

		local transliteration_page = mw.title.new(data.lang:getCanonicalName() .. " transliteration", "Wiktionary")

		if transliteration_page then
			local success, exists = pcall(function () return transliteration_page.exists end)
			if success and exists then
				translits_formatted = " [[Wiktionary:" .. data.lang:getCanonicalName() .. " transliteration|•]]" .. translits_formatted
			end
		end
	else
		translits_formatted = ""
	end

	------ Paste heads and transliterations/transcriptions. ------

	return head_parts .. translits_formatted
end


local function format_genders(data)
	if data.genders and #data.genders > 0 then
		local pos_for_cat
		if not data.nogendercat and not m_data.no_gender_cat[data.lang:getCode()] then
			local pos_category = data.pos_category:gsub("^reconstructed ", "")
			pos_for_cat = m_data.pos_for_gender_number_cat[pos_category]
		end
		local text, cats = require("Module:gender and number").format_genders(data.genders, data.lang, pos_for_cat)
		for _, cat in ipairs(cats) do
			table.insert(data.categories, cat)
		end
		return "&nbsp;" .. text
	else
		return ""
	end
end


local function format_inflection_parts(data, parts)
	for j, part in ipairs(parts) do
		if type(part) ~= "table" then
			part = {term = part}
		end

		local partaccel = part.accel
		local face = part.hypothetical and "hypothetical" or "bold"

		-- Here the final part 'or data.nolink' allows to have 'nolink=true'
		-- right into the 'data' table to disable links of the entire headword
		-- when inflected forms aren't entry-worthy, e.g.: in Vulgar Latin
		local nolink = part.hypothetical or part.nolink or data.nolink

		local formatted
		if part.label then
			-- FIXME: There should be a better way of italicizing a label. As is, this isn't customizable.
			formatted = "<i>" .. part.label .. "</i>"
		else
			-- Convert the term into a full link. Don't show a transliteration here unless enable_auto_translit is
			-- requested, either at the `parts` level (i.e. per inflection) or at the `data.inflections` level (i.e.
			-- specified for all inflections). This is controllable in {{head}} using autotrinfl=1 for all inflections,
			-- or fNautotr=1 for an individual inflection (remember that a single inflection may be associated with
			-- multiple terms). The reason for doing this is to avoid clutter in headword lines by default in languages
			-- where the script is relatively straightforward to read by learners (e.g. Greek, Russian), but allow it
			-- to be enabled in languages with more complex scripts (e.g. Arabic).
			formatted = get_module("links").full_link(
				{
					term = not nolink and part.term or nil,
					alt = part.alt or (nolink and part.term or nil),
					lang = part.lang or data.lang,
					-- FIXME, code smell in always using the first script.
					sc = part.sc or parts.sc or (not part.lang and data.heads[1].sc),
					id = part.id,
					genders = part.genders,
					tr = part.translit or (not (parts.enable_auto_translit or data.inflections.enable_auto_translit) and "-" or nil),
					ts = part.transcription,
					accel = partaccel or parts.accel,
				},
				face,
				false
				)
		end

		parts[j] = format_term_with_qualifiers_and_refs(part, formatted, j)
	end

	local parts_output

	if #parts > 0 then
		parts_output = " " .. table.concat(parts)
	elseif parts.request then
		parts_output = " <small>[please provide]</small>"
		table.insert(data.categories, "Requests for inflections in " .. data.lang:getCanonicalName() .. " entries")
	else
		parts_output = ""
	end

	return "<i>" .. parts.label .. "</i>" .. parts_output
end


-- Format the inflections following the headword.
local function format_inflections(data)
	if data.inflections and #data.inflections > 0 then
		-- Format each inflection individually.
		for key, infl in ipairs(data.inflections) do
			data.inflections[key] = format_inflection_parts(data, infl)
		end

		return " (" .. table.concat(data.inflections, ", ") .. ")"
	else
		return ""
	end
end


-- Return "lemma" if the given POS is a lemma, "non-lemma form" if a non-lemma form, or nil
-- if unknown. The POS passed in must be in its plural form ("nouns", "prefixes", etc.).
-- If you have a POS in its singular form, call pluralize() in [[Module:string utilities]] to
-- pluralize it in a smart fashion that knows when to add '-s' and when to add '-es'.
--
-- If `best_guess` is given and the POS is in neither the lemma nor non-lemma list, guess
-- based on whether it ends in " forms"; otherwise, return nil.
function export.pos_lemma_or_nonlemma(plpos, best_guess)
	-- Is it a lemma category?
	if isLemma[plpos] then
		return "lemma"
	end
	local plpos_no_recon = plpos:gsub("^reconstructed ", "")
	if isLemma[plpos_no_recon] then
		return "lemma"
	end
	-- Is it a nonlemma category?
	if isNonLemma[plpos] or isNonLemma[plpos_no_recon] then
		return "non-lemma form"
	end
	local plpos_no_mut = plpos:gsub("^mutated ", "")
	if isLemma[plpos_no_mut] or isNonLemma[plpos_no_mut] then
		return "non-lemma form"
	elseif best_guess then
		return plpos:find(" forms$") and "non-lemma form" or "lemma"
	else
		return nil
	end
end


-- Find and return the maximum index in the array `data[element]` (which may have gaps in it), and initialize it to a
-- zero-length array if unspecified. Check to make sure all keys are numeric (other than "maxindex", which is set by
-- [[Module:parameters]] for list parameters), all values are strings, and unless `allow_blank_string` is given,
-- no blank (zero-length) strings are present.
local function init_and_find_maximum_index(data, element, allow_blank_string)
	local maxind = 0
	if not data[element] then
		data[element] = {}
	end
	local typ = type(data[element])
	if typ ~= "table" then
		error(("In full_headword(), `data.%s` must be an array but is a %s"):format(element, typ))
	end
	for k, v in pairs(data[element]) do
		if k ~= "maxindex" then
			if type(k) ~= "number" then
				error(("Unrecognized non-numeric key '%s' in `data.%s`"):format(k, name))
			end
			if k > maxind then
				maxind = k
			end
			if v then
				if type(v) ~= "string" then
					error(("For key '%s' in `data.%s`, value should be a string but is a %s"):format(k, element, type(v)))
				end
				if not allow_blank_string and v == "" then
					error(("For key '%s' in `data.%s`, blank string not allowed; use 'false' for the default"):format(k, element))
				end
			end
		end
	end
	return maxind
end


--[=[
Generate a headword. This is the main function to be called externally. `data` is an object containing all the
properties needed for formatting the headword, specifically:
{
  lang = LANG_OBJECT,
  pagename = nil or STRING,
  id = nil or STRING,
  head = {
    {
	  term = nil or "HEAD",
	  tr = nil or "MANUAL_TRANSLIT",
	  ts = nil or "MANUAL_TRANSCRIPTION",
	  sc = nil or TERM_SPECIFIC_SCRIPT_OBJECT,
	  q = nil or {"LEFT_QUALIFIER", "LEFT_QUALIFIER", ...},
	  qq = nil or {"RIGHT_QUALIFIER", "RIGHT_QUALIFIER", ...},
	  refs = nil or {{text = "REF_TEXT" or "", name = nil or "REF_NAME", group = nil or "REF_GROUP"}, ...},
	  separator = nil or "SEPARATOR",
	} or "HEAD",
	...,
  },
  genders = {
    "GENDER/NUMBER-SPEC" or {spec = "GENDER/NUMBER-SPEC", qualifiers = {"QUALIFIER", "QUALIFIER", ...}},
	...
  },
  inflections = {
    enable_auto_translit = BOOLEAN,
    {
	  label = "GRAMMATICAL_CATEGORY",
	  accel = nil or {
	    form = "TAG|TAG",
		lemma = nil or "LÉMMA",
		lemma_translit = nil or "MANUAL_TRANSLIT",
		...
	  },
	  sc = nil or INFLECTION_SPECIFIC_SCRIPT_OBJECT,
	  enable_auto_translit = BOOLEAN,
      {
	    term = "INFLECTED_FORM",
		alt = nil or "DISPLAY_TEXT",
		translit = nil or "MANUAL_TRANSLIT",
		transcription = nil or "MANUAL_TRANSCRIPTION",
		genders = {
	      "GENDER/NUMBER-SPEC" or {spec = "GENDER/NUMBER-SPEC", qualifiers = {"QUALIFIER", "QUALIFIER", ...}},
		  ...
		},
	    accel = nil or {
	      form = "TAG|TAG|TAG",
		  lemma = nil or "LÉMMA",
		  lemma_translit = nil or "MANUAL_TRANSLIT",
		  ...
	    },
		lang = nil or TERM_SPECIFIC_LANG_OBJECT,
		sc = nil or TERM_SPECIFIC_SCRIPT_OBJECT,
		id = "SENSE_ID",
		q = nil or {"LEFT_QUALIFIER", "LEFT_QUALIFIER", ...},
		qq = nil or {"RIGHT_QUALIFIER", "RIGHT_QUALIFIER", ...},
		refs = nil or {{text = "REF_TEXT" or "", name = nil or "REF_NAME", group = nil or "REF_GROUP"}, ...},
		separator = nil or "SEPARATOR",
		nolink = BOOLEAN,
		hypothetical = BOOLEAN,
	  } or "INFLECTED_FORM" or {
	    label = "TEXTUAL_LABEL",
		q = nil or {"LEFT_QUALIFIER", "LEFT_QUALIFIER", ...},
		qq = nil or {"RIGHT_QUALIFIER", "RIGHT_QUALIFIER", ...},
		refs = nil or {{text = "REF_TEXT" or "", name = nil or "REF_NAME", group = nil or "REF_GROUP"}, ...},
		separator = nil or "SEPARATOR",
	  },
	  ...,
	},
	{
	  label = "GRAMMATICAL_CATEGORY",
	  request = true,
	},
	...
  },
  pos_category = "PLURAL_PART_OF_SPEECH",
  categories = {"CATEGORY", "CATEGORY", ...},
  translits = nil or {"MANUAL_TRANSLIT", nil, "MANUAL_TRANSLIT", ...},
  transcriptions = nil or {nil, "MANUAL_TRANSCRIPTION", "MANUAL_TRANSCRIPTION", ...},
  sort_key = nil or "SORT_KEY",
  id = nil or "SENSE_ID",
  force_cat_output = BOOLEAN,
  sccat = BOOLEAN,
  noposcat = BOOLEAN,
  nogendercat = BOOLEAN,
  nomultiwordcat = BOOLEAN,
  nopalindromecat = BOOLEAN,
  nolink = BOOLEAN,
}
]=]

function export.full_headword(data)

	------------ 1. Basic checks for old-style (multi-arg) calling convention. ------------

	if data.getCanonicalName then
		error("In full_headword(), the first argument `data` needs to be a Lua object (table) of properties, not a language object")
	end

	if not data.lang or type(data.lang) ~= "table" or not data.lang.getCode then
		error("In full_headword(), the first argument `data` needs to be a Lua object (table) and `data.lang` must be a language object")
	end

	if data.id and type(data.id) ~= "string" then
		error("The id in the data table should be a string.")
	end

	------------ 2. Initialize pagename etc. ------------

	local langcode = data.lang:getCode()
	local langname = data.lang:getCanonicalName()

	if data.pagename then -- for testing, doc pages, etc.
		data.title = mw.title.new(data.pagename)
		if not data.title then
			error(("Bad value for `data.pagename`: '%s'"):format(data.pagename))
		end
	else
		data.title = mw.title.getCurrentTitle()
	end

	local pagename = data.title.text
	local namespace = data.title.nsText

	-- Check the namespace against the language type.
	if namespace == "" then
		local langtype = data.lang:getType()
		if langtype == "reconstructed" then
			error("Entries in " .. langname .. " must be placed in the Reconstruction: namespace")
		elseif langtype == "appendix-constructed" then
			error("Entries in " .. langname .. " must be placed in the Appendix: namespace")
		end
	end

	------------ 3. Initialize `data.heads` table; if old-style, convert to new-style. ------------

	if type(data.heads) == "table" and type(data.heads[1]) == "table" then
		-- new-style
		if data.translits or data.transcriptions then
			error("In full_headword(), if `data.heads` is new-style (array of head objects), `data.translits` and `data.transcriptions` cannot be given")
		end
	else
		-- convert old-style `heads`, `translits` and `transcriptions` to new-style
		local maxind = math.max(
			init_and_find_maximum_index(data, "heads"),
			init_and_find_maximum_index(data, "translits", true),
			init_and_find_maximum_index(data, "transcriptions", true)
		)
		for i = 1, maxind do
			data.heads[i] = {
				term = data.heads[i],
				tr = data.translits[i],
				ts = data.transcriptions[i],
			}
		end
	end

	-- Make sure there's at least one head.
	if not data.heads[1] then
		data.heads[1] = {}
	end

	------------ 4. Initialize and validate `data.categories`, determine `pos_category` if not given, and add basic categories. ------------

	init_and_find_maximum_index(data, "categories")
	local pos_category_already_present = false
	if #data.categories > 0 then
		local escaped_langname = require("Module:pattern utilities").pattern_escape(langname)
		local matches_lang_pattern = "^" .. escaped_langname .. " "
		for _, cat in ipairs(data.categories) do
			-- Does the category begin with the language name? If not, tag it with a tracking category.
			if not cat:find(matches_lang_pattern) then
				-- [[Special:WhatLinksHere/Template:tracking/headword/no lang category]]
				-- [[Special:WhatLinksHere/Template:tracking/headword/no lang category/LANGCODE]]
				track("no lang category", langcode)
			end
		end

		-- If `pos_category` not given, try to infer it from the first specified category. If this doesn't work, we
		-- throw an error below.
		if not data.pos_category and data.categories[1]:find(matches_lang_pattern) then
			data.pos_category = data.categories[1]:gsub(matches_lang_pattern, "")
			-- Optimization to avoid inserting category already present.
			pos_category_already_present = true
		end
	end

	if not data.pos_category then
		error("`data.pos_category` not specified and could not be inferred from the categories given in "
			.. "`data.categories`. Either specify the plural part of speech in `data.pos_category` "
			.. "(e.g. \"proper nouns\") or ensure that the first category in `data.categories` is formed from the "
			.. "language's canonical name plus the plural part of speech (e.g. \"Norwegian Bokmål proper nouns\")."
			)
	end

	-- Insert a category at the beginning for the part of speech unless it's already present or `data.noposcat` given.
	if not pos_category_already_present and not data.noposcat then
		local pos_category = langname .. " " .. data.pos_category
		-- FIXME: [[User:Theknightwho]] Why is this special case here? Please add an explanatory comment.
		if pos_category ~= "Translingual Han characters" then
			table.insert(data.categories, 1, pos_category)
		end
	end

	-- Try to determine whether the part of speech refers to a lemma or a non-lemma form; if we can figure this out,
	-- add an appropriate category.
	local postype = export.pos_lemma_or_nonlemma(data.pos_category)
	if not postype then
		-- We don't know what this category is, so tag it with a tracking category.
		-- [[Special:WhatLinksHere/Template:tracking/headword/unrecognized pos]]
		-- [[Special:WhatLinksHere/Template:tracking/headword/unrecognized pos/LANGCODE]]
		track("unrecognized pos", langcode)
		-- [[Special:WhatLinksHere/Template:tracking/headword/unrecognized pos/POS]]
		-- [[Special:WhatLinksHere/Template:tracking/headword/unrecognized pos/POS/LANGCODE]]
		track("unrecognized pos/pos/" .. data.pos_category, langcode)
	elseif not data.noposcat then
		table.insert(data.categories, 1, langname .. " " .. postype .. "s")
	end

	------------ 5. Create a default headword, and add links to multiword page names. ------------

	-- Determine if term is reconstructed
	local is_reconstructed = data.lang:getType() == "reconstructed" or namespace == "Reconstruction"

	-- Create a default headword.
	local default_head = data.title.subpageText:gsub("^Unsupported titles/", "")

	local unmodified_default_head = default_head

	-- Add links to multi-word page names when appropriate
	if not m_data.no_multiword_links[langcode] and not is_reconstructed and export.head_is_multiword(default_head) then
		default_head = export.add_multiword_links(default_head)
	end

	if is_reconstructed then
		default_head = "*" .. default_head
	end

	------------ 6. Fill in missing values in `data.heads`. ------------

	-- True if any script among the headword scripts has spaces in it.
	local any_script_has_spaces = false
	-- True if any term has a redundant head= param.
	local has_redundant_head_param = false

	for _, head in ipairs(data.heads) do

		------ 6a. If missing head, replace with default head.
		if not head.term then
			head.term = default_head
		elseif head.term == default_head then
			has_redundant_head_param = true
		end

		------ 6b. Try to detect the script(s) if not provided. If a per-head script is provided, that takes precedence,
		------     otherwise fall back to the overall script if given. If neither given, autodetect the script.

		if head.sc or data.sc then
			-- Only create this closure when needed.
			local function check_redundant_sc()
				if data.noscripttracking then
					return
				end
				-- [[Special:WhatLinksHere/Template:tracking/headword/sc]]
				track("sc")

				local auto_sc = data.lang:findBestScript(head.term)
				local manual_code = head.sc:getCode()
				if manual_code == auto_sc:getCode() then
					-- [[Special:WhatLinksHere/Template:tracking/headword/sc/redundant]]
					-- [[Special:WhatLinksHere/Template:tracking/headword/sc/redundant/SCRIPTCODE]]
					track("sc/redundant", manual_code)
				else
					-- [[Special:WhatLinksHere/Template:tracking/headword/sc/needed]]
					-- [[Special:WhatLinksHere/Template:tracking/headword/sc/needed/SCRIPTCODE]]
					track("sc/needed", manual_code)
				end
			end
			if head.sc then
				-- Per-head script given.
				check_redundant_sc()
			else
				-- Overall script given.
				head.sc = data.sc
				-- Only check for redundant script if there's a single head present. If multiple heads present, it
				-- gets complicated (might be redundant for one but not the other), so don't try.
				if #data.heads == 1 then
					check_redundant_sc()
				end
			end
		else
			head.sc = data.lang:findBestScript(head.term)
		end

		-- If using a discouraged character sequence, add to maintenance category.
		if head.sc:hasNormalizationFixes() == true then
			local composed_head = unfc(head.term)
			if head.sc:fixDiscouragedSequences(composed_head) ~= composed_head then
				table.insert(data.categories, "Pages using discouraged character sequences")
			end
		end

		any_script_has_spaces = any_script_has_spaces or head.sc:hasSpaces()

		------ 6c. Create automatic transliterations for any non-Latin headwords without manual translit given
		------     (provided automatic translit is available, e.g. not in Persian or Hebrew).

		-- Make transliterations
		head.tr_manual = nil

		-- Try to generate a transliteration if necessary
		if head.tr == "-" then
			head.tr = nil
		elseif head.tr then
			head.tr_manual = true
		elseif not notranslit[langcode] and head.sc:isTransliterated() then
			head.tr = (data.lang:transliterate(get_module("links").remove_links(head.term), head.sc))
			head.tr = head.tr and mw.text.trim(head.tr)

			-- There is still no transliteration?
			-- Add the entry to a cleanup category.
			if not head.tr then
				head.tr = "<small>transliteration needed</small>"
				table.insert(data.categories, "Requests for transliteration of " .. langname .. " terms")
			end

			if head.tr then
				head.tr_manual = false
			end
		end

		-- Link to the transliteration entry for languages that require this.
		if head.tr and data.lang:link_tr() then
			head.tr = get_module("links").full_link {
				term = head.tr,
				lang = data.lang,
				sc = get_module("scripts").getByCode("Latn"),
				tr = "-"
			}
		end
	end

	------------ 7. Maybe tag the title with the appropriate script code, using the `display_title` mechanism. ------------

	-- Assumes that the scripts in "toBeTagged" will never occur in the Reconstruction namespace.
	-- (FIXME: Don't make assumptions like this, and if you need to do so, throw an error if the assumption is violated.)
	-- Avoid tagging ASCII as Hani even when it is tagged as Hani in the headword, as in [[check]]. The check for ASCII
	-- might need to be expanded to a check for any Latin characters and whitespace or punctuation.
	local display_title
	-- Where there are multiple headwords, use the script for the first. This assumes the first headword is similar to
	-- the pagename, and that headwords that are in different scripts from the pagename aren't first. This seems to be
	-- about the best we can do (alternatively we could potentially do script detection on the pagename).
	local dt_script = data.heads[1].sc
	local dt_script_code = dt_script:getCode()
	local page_non_ascii = namespace == "" and not pagename:find("^[%z\1-\127]+$")
	if page_non_ascii and toBeTagged[dt_script_code]
		or (dt_script_code == "Jpan" and (text_in_script(pagename, "Hira") or text_in_script(pagename, "Kana")))
		or (dt_script_code == "Kore" and text_in_script(pagename, "Hang")) then
		display_title = '<span class="' .. dt_script_code .. '">' .. pagename .. '</span>'
	-- Keep Han entries region-neutral in the display title.
	elseif page_non_ascii and (dt_script_code == "Hant" or dt_script_code == "Hans") then
		display_title = '<span class="Hani">' .. pagename .. '</span>'
	elseif namespace == "Reconstruction" then
		display_title, matched = rsubn(
			data.title.fullText,
			"^(Reconstruction:[^/]+/)(.+)$",
			function(before, term)
				return before ..
					require("Module:script utilities").tag_text(
						term,
						data.lang,
						dt_script
					)
			end
		)

		if matched == 0 then
			display_title = nil
		end
	end

	if display_title then
		local frame = mw.getCurrentFrame()
		frame:callParserFunction(
			"DISPLAYTITLE",
			display_title
		)
	end

	------------ 8. Insert additional categories. ------------

	if data.force_cat_output then
		-- [[Special:WhatLinksHere/Template:tracking/headword/force cat output]]
		track("force cat output")
	end

	if has_redundant_head_param then
		track("redundant-head-param", langcode)
		if not data.no_redundant_head_cat and langcode == "en" then
			table.insert(data.categories, langname .. " terms with redundant head parameter")
		end
	end

	-- If the first head is multiword (after removing links), maybe insert into "LANG multiword terms".
	if not data.nomultiwordcat and any_script_has_spaces and postype == "lemma" and not m_data.no_multiword_cat[langcode] then
		-- Check for spaces or hyphens, but exclude prefixes and suffixes.
		-- Use the pagename, not the head= value, because the latter may have extra
		-- junk in it, e.g. superscripted text that throws off the algorithm.
		local checkpattern = ".[%s%-፡]."
		if m_data.hyphen_not_multiword_sep[langcode] then
			-- Exclude hyphens if the data module states that they should for this language
			checkpattern = ".[%s፡]."
		end
		if rfind(unmodified_default_head, checkpattern) and not non_categorizable(data) then
			table.insert(data.categories, langname .. " multiword terms")
		end
	end

	if data.sccat then
		for _, head in ipairs(data.heads) do
			table.insert(data.categories, langname .. " " .. data.pos_category .. " in " .. head.sc:getDisplayForm())
		end
	end

	-- Categorise for unusual characters
	local standard = data.lang:getStandardCharacters()

	if standard then
		if ulen(data.title.subpageText) ~= 1 and not non_categorizable(data) then
			for character in rgmatch(data.title.subpageText, "([^" .. standard .. "])") do
				local upper = uupper(character)
				if not rfind(upper, "[" .. standard .. "]") then
					character = upper
				end
				table.insert(data.categories, langname .. " terms spelled with " .. character)
			end
		end
	end

	-- Categorise for palindromes
	if not data.nopalindromecat and namespace ~= "Reconstruction" and ulen(data.title.subpageText) > 2
		-- FIXME: Use of first script here seems hacky. What is the clean way of doing this in the presence of
		-- multiple scripts?
		and require("Module:palindromes").is_palindrome(data.title.subpageText, data.lang, data.heads[1].sc) then
		table.insert(data.categories, langname .. " palindromes")
	end

	if namespace == "" and data.lang:getType() ~= "reconstructed" then
		local m_links = get_module("links")
		for _, head in ipairs(data.heads) do
			if data.title.prefixedText ~= m_links.getLinkPage(m_links.remove_links(head.term), data.lang, head.sc) then
				-- [[Special:WhatLinksHere/Template:tracking/headword/pagename spelling mismatch]]
				-- [[Special:WhatLinksHere/Template:tracking/headword/pagename spelling mismatch/LANGCODE]]
				track("pagename spelling mismatch", data.lang:getCode())
				break
			end
		end
	end

	------------ 9. Format and return headwords, genders, inflections and categories. ------------

	-- Format and return all the gathered information. This may add more categories (e.g. gender/number categories),
	-- so make sure we do it before evaluating `data.categories`.
	local text =
		format_headword(data) ..
		format_genders(data) ..
		format_inflections(data)

	return
		text ..
		require("Module:utilities").format_categories(
			data.categories, data.lang, data.sort_key, nil,
			data.force_cat_output or test_force_categories, data.heads[1].sc
		)
end

return export