Module:names



local m_languages = require("Module:languages")
local m_links = require("Module:links")
local m_utilities = require("Module:utilities")
local m_table = require("Module:table")

local export = {}

local enlang = m_languages.getByCode("en")

local rfind = mw.ustring.find
local rmatch = mw.ustring.match
local rsubn = mw.ustring.gsub
local rsplit = mw.text.split

local force_cat = false -- for testing

-- 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

-- Used in category code
export.personal_name_types = {
	"surnames", "patronymics", "given names",
	"male given names", "female given names", "unisex given names",
	"diminutives of male given names", "diminutives of female given names",
	"diminutives of unisex given names",
	"augmentatives of male given names", "augmentatives of female given names",
	"augmentatives of unisex given names"
}


local translit_name_type_list = {
	"surname", "male given name", "female given name", "unisex given name",
	"patronymic"
}
local translit_name_types = m_table.listToSet(translit_name_type_list)

local param_mods = {"t", "alt", "tr", "ts", "pos", "lit", "id", "sc", "g", "q", "eq"}
local param_mod_set = m_table.listToSet(param_mods)



--[=[
Parse a term and associated properties. This works with parameters of the form 'Karlheinz' or
'Kunigunde<q:medieval, now rare>' or 'non:Óláfr' or 'ru:Фру́нзе<tr:Frúnzɛ><q:rare>' where the modifying properties
are contained in <...> specifications after the term. `term` is the full parameter value including any angle brackets
and colons; `pname` is the name of the parameter that this value comes from, for error purposes; `deflang` is a
language object used in the return value when the language isn't specified (e.g. in the examples 'Karlheinz' and
'Kunigunde<q:medieval, now rare>' above); `allow_explicit_lang` indicates whether the language can be explicitly given
(e.g. in the examples 'non:Óláfr' or 'ru:Фру́нзе<tr:Frúnzɛ><q:rare>' above).

Normally the return value is an object with properties '.term' (a terminfo object that can be passed to full_link() in
[[Module:links]]) and '.q' (a qualifier). However, if `allow_multiple_terms` is given, multiple comma-separated names
can be given in `term`, and the return value is a list of objects of the form described just above.
]=]
local function parse_term_with_annotations(term, pname, deflang, allow_explicit_lang, allow_multiple_terms)
	local function parse_single_run_with_annotations(run)
		local function parse_err(msg)
			error(msg .. ": " .. pname .. "= " .. table.concat(run))
		end
		if #run == 1 and run[1] == "" then
			error("Blank form for param '" .. pname .. "' not allowed")
		end
		local termobj = {term = {}}
		local lang, form = run[1]:match("^(.-):(.*)$")
		if lang then
			if not allow_explicit_lang then
				parse_err("Explicit language '" .. lang .. "' not allowed for this parameter")
			end
			termobj.term.lang = m_languages.getByCode(lang, pname, "allow etym lang")
			termobj.term.term = form
		else
			termobj.term.lang = deflang
			termobj.term.term = run[1]
		end

		for i = 2, #run - 1, 2 do
			if run[i + 1] ~= "" then
				parse_err("Extraneous text '" .. run[i + 1] .. "' after modifier")
			end
			local modtext = run[i]:match("^<(.*)>$")
			if not modtext then
				parse_err("Internal error: Modifier '" .. modtext .. "' isn't surrounded by angle brackets")
			end
			local prefix, arg = modtext:match("^([a-z]+):(.*)$")
			if not prefix then
				parse_err("Modifier " .. run[i] .. " lacks a prefix, should begin with one of '" ..
					table.concat(param_mods, ":', '") .. ":'")
			end
			if param_mod_set[prefix] then
				local obj_to_set
				if prefix == "q" or prefix == "eq" then
					obj_to_set = termobj
				else
					obj_to_set = termobj.term
				end
				if obj_to_set[prefix] then
					parse_err("Modifier '" .. prefix .. "' occurs twice, second occurrence " .. run[i])
				end
				if prefix == "t" then
					termobj.term.gloss = arg
				elseif prefix == "g" then
					termobj.term.genders = rsplit(arg, ",")
				elseif prefix == "sc" then
					termobj.term.sc = require("Module:scripts").getByCode(arg, pname)
				elseif prefix == "eq" then
					termobj.eq = parse_term_with_annotations(arg, pname .. ".eq", enlang, false, "allow multiple terms")
				else
					obj_to_set[prefix] = arg
				end
			else
				parse_err("Unrecognized prefix '" .. prefix .. "' in modifier " .. run[i])
			end
		end
		return termobj
	end

	local iut = require("Module:inflection utilities")
	local run = iut.parse_balanced_segment_run(term, "<", ">")
	if allow_multiple_terms then
		local comma_separated_runs = iut.split_alternating_runs(run, "%s*,%s*")
		local termobjs = {}
		for _, comma_separated_run in ipairs(comma_separated_runs) do
			table.insert(termobjs, parse_single_run_with_annotations(comma_separated_run))
		end
		return termobjs
	else
		return parse_single_run_with_annotations(run)
	end
end


--[=[
Link a single term. If `do_language_link` is given and a given term's language is English, the link will be constructed
using language_link() in [[Module:links]]; otherwise, with full_link(). Each term in `terms` is an object as returned
by parse_term_with_annotations(), i.e. it contains fields '.term' (a terminfo structure suitable for passing to
full_link() or language_link()), optional '.q' (a qualifier) and optional '.eq' (a list of objects of the same form as
`termobj`).
]=]
local function link_one_term(termobj, do_language_link)
	termobj.term.lang = m_languages.getNonEtymological(termobj.term.lang)
	local link
	if do_language_link and termobj.term.lang:getCode() == "en" then
		link = m_links.language_link(termobj.term, nil, true)
	else
		link = m_links.full_link(termobj.term, nil, true)
	end
	if termobj.q then
		link = require("Module:qualifier").format_qualifier(termobj.q) .. " " .. link
	end
	if termobj.eq then
		local eqtext = {}
		for _, eqobj in ipairs(termobj.eq) do
			table.insert(eqtext, link_one_term(eqobj, true))
		end
		link = link .. " [=" .. m_table.serialCommaJoin(eqtext, {conj = "or"}) .. "]"
	end
	return link
end


--[=[
Link the terms in `terms`, and join them using the conjunction in `conj` (defaulting to "or"). Joining is done using
serialCommaJoin() in [[Module:table]], so that e.g. two terms are joined as "TERM or TERM" while three terms are joined
as "TERM, TERM or TERM" with special CSS spans before the final "or" to allow an "Oxford comma" to appear if configured
appropriately. (However, if `conj` is the special value ", ", joining is done directly using that value.)
If `include_langname` is given, the language of the first term will be prepended to the joined terms. If
`do_language_link` is given and a given term's language is English, the link will be constructed using language_link()
in [[Module:links]]; otherwise, with full_link(). Each term in `terms` is an object as returned by
parse_term_with_annotations(), i.e. it contains fields '.term' (a terminfo structure suitable for passing to full_link()
or language_link()), optional '.q' (a qualifier) and optional '.eq' (a list of objects of the same form as in `terms`).
]=]
local function join_terms(terms, include_langname, do_language_link, conj)
	local links = {}
	local langnametext
	for _, termobj in ipairs(terms) do
		if include_langname and not langnametext then
			langnametext = termobj.term.lang:getCanonicalName() .. " "
		end
		table.insert(links, link_one_term(termobj, do_language_link))
	end
	local joined_terms
	if conj == ", " then
		joined_terms = table.concat(links, conj)
	else
		joined_terms = m_table.serialCommaJoin(links, {conj = conj or "or"})
	end
	return (langnametext or "") .. joined_terms
end


--[=[
Gather the parameters for multiple names and link each name using full_link() (for foreign names) or language_link()
(for English names), joining the names using serialCommaJoin() in [[Module:table]] with the conjunction `conj`
(defaulting to "or"). (However, if `conj` is the special value ", ", joining is done directly using that value.)
This can be used, for example, to fetch and join all the masculine equivalent names for a feminine given name. Each
name is specified using parameters beginning with `pname` in `args`, e.g. "m", "m2", "m3", etc. `lang` is a language
object specifying the language of the names (defaulting to English), for use in linking them. If `allow_explicit_lang`
is given, the language of the terms can be specified explicitly by prefixing a term with a language code, e.g.
'sv:Björn' or 'la:[[Nicolaus|Nīcolāī]]'. This function assumes that the parameters have already been parsed by
[[Module:parameters]] and gathered into lists, so that e.g. all "mN" parameters are in a list in args["m"].
]=]
local function join_names(lang, args, pname, conj, allow_explicit_lang)
	local termobjs = {}
	local do_language_link = false
	if not lang then
		lang = enlang
		do_language_link = true
	end

	for i, term in ipairs(args[pname]) do
		table.insert(termobjs, parse_term_with_annotations(term, pname .. (i == 1 and "" or i), lang, allow_explicit_lang))
	end
	return join_terms(termobjs, nil, do_language_link, conj), #termobjs
end


local function get_eqtext(args)
	local eqsegs = {}
	local lastlang = nil
	local last_eqseg = {}
	for i, term in ipairs(args.eq) do
		local termobj = parse_term_with_annotations(term, "eq" .. (i == 1 and "" or i), enlang, "allow explicit lang")
		local termlang = termobj.term.lang:getCode()
		if lastlang and lastlang ~= termlang then
			if #last_eqseg > 0 then
				table.insert(eqsegs, last_eqseg)
			end
			last_eqseg = {}
		end
		lastlang = termlang
		table.insert(last_eqseg, termobj)
	end
	if #last_eqseg > 0 then
		table.insert(eqsegs, last_eqseg)
	end
	local eqtextsegs = {}
	for _, eqseg in ipairs(eqsegs) do
		table.insert(eqtextsegs, join_terms(eqseg, "include langname"))
	end
	return m_table.serialCommaJoin(eqtextsegs, {conj = "or"})
end


local function get_fromtext(lang, args)
	local catparts = {}
	local fromsegs = {}
	local i = 1

	local function parse_from(from)
		local unrecognized = false
		local prefix, suffix
		if from == "surnames" then
			prefix = "transferred from the "
			suffix = "surname"
			table.insert(catparts, from)
		elseif from == "place names" then
			prefix = "transferred from the "
			suffix = "place name"
			table.insert(catparts, from)
		elseif from == "coinages" then
			prefix = "originating "
			suffix = "as a coinage"
			table.insert(catparts, from)
		elseif from == "the Bible" then
			prefix = "originating "
			suffix = "from the Bible"
			table.insert(catparts, from)
		else
			prefix = "from "
			if from:find(":") then
				local termobj = parse_term_with_annotations(from, "from" .. (i == 1 and "" or i), lang, "allow explicit lang")
				local fromlangname = ""
				if termobj.term.lang:getCode() ~= lang:getCode() then
					-- If name is derived from another name in the same language, don't include lang name after text "from "
					-- or create a category like "German male given names derived from German".
					local canonical_name = termobj.term.lang:getCanonicalName()
					fromlangname = canonical_name .. " "
					table.insert(catparts, canonical_name)
				end
				termobj.term.lang = m_languages.getNonEtymological(termobj.term.lang)
				suffix = fromlangname .. link_one_term(termobj)
			elseif from:find(" languages$") then
				local family = from:match("^(.*) languages$")
				if require("Module:families").getByCanonicalName(family) then
					table.insert(catparts, from)
				else
					unrecognized = true
				end
				suffix = "the " .. from
			else
				if m_languages.getByCanonicalName(from, nil, "allow etym") then
					table.insert(catparts, from)
				else
					unrecognized = true
				end
				suffix = from
			end
		end
		return prefix, suffix
	end

	local last_fromseg = nil
	while args.from[i] do
		local rawfrom = args.from[i]
		local froms = rsplit(rawfrom, "%s+<%s+")
		if #froms == 1 then
			local prefix, suffix = parse_from(froms[1])
			if last_fromseg and (last_fromseg.has_multiple_froms or last_fromseg.prefix ~= prefix) then
				table.insert(fromsegs, last_fromseg)
				last_fromseg = nil
			end
			if not last_fromseg then
				last_fromseg = {prefix = prefix, suffixes = {}}
			end
			table.insert(last_fromseg.suffixes, suffix)
		else
			if last_fromseg then
				table.insert(fromsegs, last_fromseg)
				last_fromseg = nil
			end
			local first_suffixpart = ""
			local rest_suffixparts = {}
			for j, from in ipairs(froms) do
				local prefix, suffix = parse_from(from)
				if j == 1 then
					first_suffixpart = prefix .. suffix
				else
					table.insert(rest_suffixparts, prefix .. suffix)
				end
			end
			local full_suffix = first_suffixpart .. " [in turn " .. table.concat(rest_suffixparts, ", in turn ") .. "]"
			last_fromseg = {prefix = "", has_multiple_froms = true, suffixes = {full_suffix}}
		end
		i = i + 1
	end
	table.insert(fromsegs, last_fromseg)
	local fromtextsegs = {}
	for _, fromseg in ipairs(fromsegs) do
		table.insert(fromtextsegs, fromseg.prefix ..  m_table.serialCommaJoin(fromseg.suffixes, {conj = "or"}))
	end
	return m_table.serialCommaJoin(fromtextsegs, {conj = "or"}), catparts
end


-- The entry point for {{given name}}.
function export.given_name(frame)
	local parent_args = frame:getParent().args
	local compat = parent_args.lang
	local offset = compat and 0 or 1

	local params = {
		[compat and "lang" or 1] = { required = true, default = "und" },
		["gender"] = { default = "unknown-gender" },
		[1 + offset] = { alias_of = "gender", default = "unknown-gender" },
		-- second gender
		["or"] = {},
		["usage"] = {},
		["origin"] = {},
		["popular"] = {},
		["populartype"] = {},
		["meaning"] = { list = true },
		["meaningtype"] = {},
		["q"] = {},
		-- initial article: A or An
		["A"] = {},
		["sort"] = {},
		["from"] = { list = true },
		[2 + offset] = { alias_of = "from", list = true },
		["fromtype"] = {},
		["xlit"] = { list = true },
		["eq"] = { list = true },
		["eqtype"] = {},
		["varof"] = { list = true },
		["varoftype"] = {},
		["var"] = { alias_of = "varof", list = true },
		["vartype"] = { alias_of = "varoftype" },
		["varform"] = { list = true },
		["dimof"] = { list = true },
		["dimoftype"] = {},
		["dim"] = { alias_of = "dimof", list = true },
		["dimtype"] = { alias_of = "dimoftype" },
		["diminutive"] = { alias_of = "dimof", list = true },
		["diminutivetype"] = { alias_of = "dimoftype" },
		["dimform"] = { list = true },
		["blend"] = { list = true },
		["blendtype"] = {},
		["m"] = { list = true },
		["mtype"] = {},
		["f"] = { list = true },
		["ftype"] = {},
	}

	local args = require("Module:parameters").process(parent_args, params)
	
	local textsegs = {}
	local lang = m_languages.getByCode(args[compat and "lang" or 1], compat and "lang" or 1)

	local function fetch_typetext(param)
		return args[param] and args[param] .. " " or ""
	end

	local dimoftext, numdims = join_names(lang, args, "dimof")
	local xlittext = join_names(nil, args, "xlit")
	local blendtext = join_names(lang, args, "blend", "and")
	local varoftext = join_names(lang, args, "varof")
	local mtext = join_names(lang, args, "m")
	local ftext = join_names(lang, args, "f")
	local varformtext, numvarforms = join_names(lang, args, "varform", ", ")
	local dimformtext, numdimforms = join_names(lang, args, "dimform", ", ")
	local meaningsegs = {}
	for _, meaning in ipairs(args.meaning) do
		table.insert(meaningsegs, '"' .. meaning .. '"')
	end
	local meaningtext = m_table.serialCommaJoin(meaningsegs, {conj = "or"})
	local eqtext = get_eqtext(args)

	table.insert(textsegs, "<span class='use-with-mention'>")
	local dimtype = args.dimtype
	local article = args.A or
		dimtype and rfind(dimtype, "^[aeiouAEIOU]") and "An" or
		args.gender == "unknown-gender" and "An" or
		"A"

	table.insert(textsegs, article .. " ")
	if numdims > 0 then
		table.insert(textsegs,
			(dimtype and dimtype .. " " or "") ..
			"[[wikt:diminutive|diminutive]]" ..
			(xlittext ~= "" and ", " .. xlittext .. "," or "") ..
			" of the ")
	end
	local genders = {}
	table.insert(genders, args.gender)
	table.insert(genders, args["or"])
	table.insert(textsegs, table.concat(genders, " or ") .. " ")
	table.insert(textsegs, numdims > 1 and "[[wikt:given name|given names]]" or
		"[[wikt:given name|given name]]")
	local need_comma = false
	if numdims > 0 then
		table.insert(textsegs, " " .. dimoftext)
		need_comma = true
	elseif xlittext ~= "" then
		table.insert(textsegs, ", " .. xlittext)
		need_comma = true
	end
	local from_catparts = {}
	if #args.from > 0 then
		if need_comma then
			table.insert(textsegs, ",")
		end
		need_comma = true
		table.insert(textsegs, " " .. fetch_typetext("fromtype"))
		local textseg, this_catparts = get_fromtext(lang, args)
		for _, catpart in ipairs(this_catparts) do
			m_table.insertIfNot(from_catparts, catpart)
		end
		table.insert(textsegs, textseg)
	end
	
	if meaningtext ~= "" then
		if need_comma then
			table.insert(textsegs, ",")
		end
		need_comma = true
		table.insert(textsegs, " " .. fetch_typetext("meaningtype") .. "meaning " .. meaningtext)
	end
	if args.origin then
		if need_comma then
			table.insert(textsegs, ",")
		end
		need_comma = true
		table.insert(textsegs, " of " .. args.origin .. " origin")
	end
	if args.usage then
		if need_comma then
			table.insert(textsegs, ",")
		end
		need_comma = true
		table.insert(textsegs, " of " .. args.usage .. " usage")
	end
	if varoftext ~= "" then
		table.insert(textsegs, ", " ..fetch_typetext("varoftype") .. "variant of " .. varoftext)
	end
	if blendtext ~= "" then
		table.insert(textsegs, ", " .. fetch_typetext("blendtype") .. "blend of " .. blendtext)
	end
	if args.popular then
		table.insert(textsegs, ", " .. fetch_typetext("populartype") .. "popular " .. args.popular)
	end
	if mtext ~= "" then
		table.insert(textsegs, ", " .. fetch_typetext("mtype") .. "masculine equivalent " .. mtext)
	end
	if ftext ~= "" then
		table.insert(textsegs, ", " .. fetch_typetext("ftype") .. "feminine equivalent " .. ftext)
	end
	if eqtext ~= "" then
		table.insert(textsegs, ", " .. fetch_typetext("eqtype") .. "equivalent to " .. eqtext)
	end
	if args.q then
		table.insert(textsegs, ", " .. args.q)
	end
	if varformtext ~= "" then
		table.insert(textsegs, "; variant form" .. (numvarforms > 1 and "s" or "") .. " " .. varformtext)
	end
	if dimformtext ~= "" then
		table.insert(textsegs, "; diminutive form" .. (numdimforms > 1 and "s" or "") .. " " .. dimformtext)
	end
	table.insert(textsegs, "</span>")

	local categories = {}
	local langname = lang:getCanonicalName() .. " "
	local function insert_cats(isdim)
		if isdim == "" then
			-- No category such as "English diminutives of given names"
			table.insert(categories, langname .. isdim .. "given names")
		end
		local function insert_cats_gender(g)
			if g == "unknown-gender" then
				return
			end
			if g ~= "male" and g ~= "female" and g ~= "unisex" then
				error("Unrecognized gender: " .. g)
			end
			if g == "unisex" then
				insert_cats_gender("male")
				insert_cats_gender("female")
			end
			table.insert(categories, langname .. isdim .. g .. " given names")
			for _, catpart in ipairs(from_catparts) do
				table.insert(categories, langname .. isdim .. g .. " given names from " .. catpart)
			end
		end
		insert_cats_gender(args.gender)
		if args["or"] then
			insert_cats_gender(args["or"])
		end
	end
	insert_cats("")
	if numdims > 0 then
		insert_cats("diminutives of ")
	end

	return table.concat(textsegs, "") ..
		m_utilities.format_categories(categories, lang, args.sort, nil, force_cat)
end

-- The entry point for {{name translit}} and {{name respelling}}.
function export.name_translit(frame)
    local iparams = {
        ["desctext"] = {required = true},
    }
    local iargs = require("Module:parameters").process(frame.args, iparams)

	local parent_args = frame:getParent().args

	local params = {
		[1] = { required = true, default = "en" },
		[2] = { required = true, default = "ru" },
		[3] = { list = true },
		["type"] = { required = true, list = true, default = "patronymic" },
		["alt"] = { list = true, allow_holes = true },
		["t"] = { list = true, allow_holes = true },
		["gloss"] = { list = true, alias_of = "t", allow_holes = true },
		["tr"] = { list = true, allow_holes = true },
		["ts"] = { list = true, allow_holes = true },
		["id"] = { list = true, allow_holes = true },
		["sc"] = { list = true, allow_holes = true },
		["g"] = { list = true, allow_holes = true },
		["q"] = { list = true, allow_holes = true },
		["xlit"] = { list = true, allow_holes = true },
		["eq"] = { list = true, allow_holes = true },
		["dim"] = { type = "boolean" },
		["aug"] = { type = "boolean" },
		["nocap"] = { type = "boolean" },
		["sort"] = {},
	}
	
	local args = require("Module:parameters").process(parent_args, params)
	local lang = m_languages.getByCode(args[1], 1)
	local sources = {}
	local source_non_etym_langs = {}
	for _, source in ipairs(rsplit(args[2], "%s*,%s*")) do
		local sourcelang = m_languages.getByCode(source, 2, "allow etym")
		table.insert(sources, sourcelang)
		table.insert(source_non_etym_langs, m_languages.getNonEtymological(sourcelang))
	end

	local nametypes = {}
	for _, typearg in ipairs(args["type"]) do
		for _, ty in ipairs(rsplit(typearg, "%s*,%s*")) do
			if not translit_name_types[ty] then
				local quoted_types = {}
				for _, nametype in ipairs(translit_name_type_list) do
					table.insert(quoted_types, "'" .. nametype .. "'")
				end
				error("Unrecognized type '" .. ty .. "': It should be one of " ..
					m_table.serialCommaJoin(quoted_types, {conj = "or"}))
			end
			table.insert(nametypes, ty)
		end
	end

	-- Find the maximum index among any of the list parameters, to determine how many names are given.
	local maxmaxindex = #args[3]
	for k, v in pairs(args) do
		if type(v) == "table" and v.maxindex and v.maxindex > maxmaxindex then
			maxmaxindex = v.maxindex
		end
	end

	local textsegs = {}
	table.insert(textsegs, "<span class='use-with-mention'>")
	local desctext = iargs.desctext
	if not args.nocap then
		desctext = mw.getContentLanguage():ucfirst(desctext)
	end
	table.insert(textsegs, desctext)
	table.insert(textsegs, " of ")
	local langsegs = {}
	for i, source in ipairs(sources) do
		local sourcename = source:getCanonicalName()
		local function get_source_link()
			if args[3][1] then
				return m_links.language_link {
					lang = source_non_etym_langs[i], term = args[3][1], alt = sourcename, tr = "-"
				}
			else
				return sourcename
			end
		end
		
		if i == 1 then
			-- If at least one name is given, we say "A transliteration of the LANG surname FOO", linking LANG to FOO.
			-- Otherwise we say "A transliteration of a LANG surname".
			if maxmaxindex > 0 then
				table.insert(langsegs, "the " .. get_source_link())
			else
				table.insert(langsegs, require("Module:string utilities").add_indefinite_article(sourcename))
			end
		else
			table.insert(langsegs, get_source_link())
		end
	end
	table.insert(textsegs, m_table.serialCommaJoin(langsegs, {conj = "or"}))
	table.insert(textsegs, " " .. m_table.serialCommaJoin(nametypes))
	if args.dim then
		table.insert(textsegs, " [[wikt:diminutive|diminutive]]")
	elseif args.aug then
		table.insert(textsegs, " [[wikt:augmentative|augmentative]]")
	end
	table.insert(textsegs, " ")
	local names = {}

	local embedded_comma = false

	for i = 1, maxmaxindex do
		local sc = require("Module:scripts").getByCode(args["sc"][i], true)
		
		local terminfo = {
			lang = source_non_etym_langs[1], term = args[3][i], alt = args["alt"][i], id = args["id"][i], sc = sc,
			tr = args["tr"][i], ts = args["ts"][i], gloss = args["t"][i],
			genders = args["g"][i] and rsplit(args["g"][i], ",") or {}
		}
		local linked_term = m_links.full_link(terminfo, "term")
		if  args["q"][i] then
			linked_term = require("Module:qualifier").format_qualifier(args["q"][i]) .. " " .. linked_term
		end
		if args["xlit"][i] then
			embedded_comma = true
			linked_term = linked_term .. ", " .. m_links.language_link({ lang = m_languages.getByCode("en"), term = args["xlit"][i] })
		end
		if args["eq"][i] then
			embedded_comma = true
			linked_term = linked_term .. ", equivalent to " .. m_links.language_link({ lang = m_languages.getByCode("en"), term = args["eq"][i] })
		end
		table.insert(names, linked_term)
	end

	if embedded_comma then
		table.insert(textsegs, table.concat(names, "; or of "))
	else
		table.insert(textsegs, m_table.serialCommaJoin(names, {conj = "or"}))
	end
	table.insert(textsegs, "</span>")

	local categories = {}
	for _, nametype in ipairs(nametypes) do
		local function insert_cats(isdim)
			local function insert_cats_type(ty)
				if ty == "unisex given name" then
					insert_cats_type("male given name")
					insert_cats_type("female given name")
				end
				for i, source in ipairs(sources) do
					table.insert(categories, lang:getCode() .. ":" .. source:getCanonicalName() .. " " .. isdim .. ty .. "s")
					local sourcelang = source_non_etym_langs[i]
					if source:getCode() ~= sourcelang:getCode() then
						-- etymology language
						table.insert(categories, lang:getCode() .. ":" .. sourcelang:getCanonicalName() .. " " .. isdim .. ty .. "s")
					end
				end
			end
			insert_cats_type(nametype)
		end
		insert_cats("")
		if args.dim then
			insert_cats("diminutives of ")
		end
		if args.aug then
			insert_cats("augmentatives of ")
		end
	end

	return table.concat(textsegs, "") ..
		m_utilities.format_categories(categories, lang, args.sort, nil, force_cat)
end

return export