此模組用於實現模板{{人名}}


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 rsubn = mw.ustring.gsub
local rsplit = mw.text.split

local force_cat = false -- for testing

--[=[

FIXME:

1. from=the Bible (DONE)
2. origin=18th century [DONE]
3. popular= (DONE)
4. varoftype= (DONE)
5. eqtype= [DONE]
6. dimoftype= [DONE]
7. from=de:Elisabeth (same language) (DONE)
8. blendof=, blendof2= [DONE]
9. varform, dimform [DONE]
10. from=English < Latin [DONE]
11. usage=rare -> categorize as rare?
12. dimeq= (also vareq=?) [DONE]
13. fromtype= [DONE]
14. <tr:...> and similar params [DONE]
]=]

-- 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 = {
	"姓氏", "男性姓氏", "女性姓氏", "通性姓氏",
	"父名", "母名", 
	"名字", "男性名字", "女性名字", "中性名字",
	"男性名字指小詞", "女性名字指小詞",
	"中性名字指小詞",
	"男性名字指大詞", "女性名字指大詞",
	"中性名字指大詞",
}


local translit_name_type_list = {
	"姓氏", "男性名字", "女性名字", "中性名字",
	"父名"
}
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)


local function track(page)
	require("Module:debug").track("names/" .. page)
end



--[=[
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 and lang ~= "w" 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)
	local link
	if do_language_link and termobj.term.lang:getCode() == "en" then
		link = m_links.language_link(termobj.term)
	else
		link = m_links.full_link(termobj.term)
	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, {"或"}) .. "]"
	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 "或"})
	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, {"或"})
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 == "名字" then from = "人名" end
  		if from == "姓氏" or from == "人名" or from == "暱稱" or from == "地名" or from == "普通名詞" or from == "月份名" then
			prefix = "轉變自"
			suffix = from:gsub("s$", "")
			table.insert(catparts, from)
  		elseif from == "父名" or from == "母名" or from == "新詞" then
			prefix = "來自"
			suffix = from:gsub("s$", "")
			table.insert(catparts, from)
  		elseif from == "職業" or from == "民族名稱" then
			prefix = "來自"
			suffix = from:gsub("$", "")
			table.insert(catparts, from)
		elseif from == "聖經" then
			prefix = "來自"
			suffix = "聖經"
			table.insert(catparts, from)
		else
			prefix = "源自"
			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
				suffix = fromlangname .. link_one_term(termobj)
			else
				local family = from:match("^(.+)[語语]系$") or -- L10N
							   from:match("^(.+)[語语]族$") or
							   from:match("^(.+)[語语]支$") or
							   from:match("^(.+)[語语][組组]$") or
							   from:match("^(.+)[語语][門门]$") or
							   from:match("^(.+)[諸诸][語语]言$")
				if family then
					if require("Module:families").getByCanonicalName(family) then
						table.insert(catparts, from)
					else
						unrecognized = true
					end
					suffix = from
				else
					if m_languages.getByCanonicalName(from, nil, "allow etym") then
						table.insert(catparts, from)
					else
						unrecognized = true
					end
					suffix = from
				end
			end
		end
		if unrecognized then
			track("unrecognized from")
			track("unrecognized from/" .. from)
		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, {"或"}))
	end
	return m_table.serialCommaJoin(fromtextsegs, {"或"}), 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 lang_index = compat and "lang" or 1

	local params = {
		[lang_index] = { required = true, default = "und" },
		["gender"] = { default = "性別不明" },
		[1 + offset] = { alias_of = "gender", default = "性別不明" },
		["or"] = {}, -- second gender
		["orq"] = {}, -- second gender qualifier
		["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 },
		["augof"] = { list = true },
		["augoftype"] = {},
		["aug"] = { alias_of = "augof", list = true },
		["augtype"] = { alias_of = "augoftype" },
		["augmentative"] = { alias_of = "augof", list = true },
		["augmentativetype"] = { alias_of = "augoftype" },
		["augform"] = { 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 langcode = args[lang_index]
	local lang = m_languages.getByCode(langcode, lang_index)

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

	local dimoftext, numdims = join_names(lang, args, "dimof")
	local augoftext, numaugs = join_names(lang, args, "augof")
	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 augformtext, numaugforms = join_names(lang, args, "augform", ", ")
	local meaningsegs = {}
	for _, meaning in ipairs(args.meaning) do
		table.insert(meaningsegs, '“' .. meaning .. '”')
	end
	local meaningtext = m_table.serialCommaJoin(meaningsegs, {"或"})
	local eqtext = get_eqtext(args)

	table.insert(textsegs, "<span class='use-with-mention'>")
	local dimtype = args.dimtype
	local augtype = args.augtype
	local article = args.A
	local need_an = false
	if not article then
		local get_indefinite_article = require("Module:string utilities").get_indefinite_article
		if numdims > 0 then
			article = get_indefinite_article(dimtype)
		elseif numaugs > 0 then
			if augtype then
				article = get_indefinite_article(augtype)
			else
				article = "an" -- "augmentative" needs an article
			end
		else
			article = args.gender == "unknown-gender" and "an" or "a"
		end
		if langcode == "en" then
			article = mw.getContentLanguage():ucfirst(article)
		end
	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 .. (args.from[1]:find(":") and " 的" or "的")) -- L10N
	end
	local genders = {}
	table.insert(genders, args.gender)
	if args["or"] then
		table.insert(genders, (args.orq and "(" .. args.orq .. ") " or "") .. args["or"])
	end
	table.insert(textsegs, table.concat(genders, "或"))
	table.insert(textsegs, "[[人名]]") -- 移除冠詞
	if numdims > 0 then -- L10N: 調整順序
		if dimoftext ~= "" then -- 先放名字
			table.insert(textsegs, " " .. dimoftext)
		end
		
		table.insert(textsegs, -- 再放「的指小詞」
			" " .. (dimtype and dimtype .. " " or "") ..
			"的[[指小詞]]" ..
			(xlittext ~= "" and "," .. xlittext .. "," or ""))
	elseif numaugs > 0 then
		if augoftext ~= "" then -- 先放名字
			table.insert(textsegs, " " .. augoftext)
		end
		
		table.insert(textsegs, -- 再放「的指大詞」
			" " .. (augtype and augtype .. " " or "") ..
			"的[[指大詞]]" ..
			(xlittext ~= "" and "," .. xlittext .. "," or ""))

	elseif xlittext ~= "" then
		table.insert(textsegs, "," .. xlittext)
		need_comma = true
	end
	
	if meaningtext ~= "" then
		if need_comma then
			table.insert(textsegs, ",")
		end
		need_comma = true
		table.insert(textsegs, " " .. fetch_typetext("meaningtype") .. "表示" .. meaningtext)
	end
	if args.origin then
		if need_comma then
			table.insert(textsegs, ",")
		end
		need_comma = true
		table.insert(textsegs, "來自" .. args.origin)
	end
	if args.usage then
		if need_comma then
			table.insert(textsegs, ",")
		end
		need_comma = true
		table.insert(textsegs, "用於" .. args.usage)
	end
	if varoftext ~= "" then
		table.insert(textsegs, "," .. varoftext .. "的" .. fetch_typetext("varoftype") .. "變體")
	end
	if blendtext ~= "" then
		table.insert(textsegs, "," .. blendtext .. "的" .. fetch_typetext("blendtype") .. "混成詞")
	end
	if args.popular then
		table.insert(textsegs, "," .. fetch_typetext("populartype") .. "通俗" .. args.popular)
	end
	if mtext ~= "" then
		table.insert(textsegs, "," .. fetch_typetext("mtype") .. "陽性等價詞 " .. mtext)
	end
	if ftext ~= "" then
		table.insert(textsegs, "," .. fetch_typetext("ftype") .. "陰性等價詞 " .. ftext)
	end
	if eqtext ~= "" then
		table.insert(textsegs, "," .. fetch_typetext("eqtype") .. "等價於" .. eqtext)
	end
	if args.q then
		table.insert(textsegs, "," .. args.q)
	end
	if varformtext ~= "" then
		table.insert(textsegs, ";變體形式 " .. varformtext)
	end
	if dimformtext ~= "" then
		table.insert(textsegs, ";指小詞 " .. dimformtext)
	end
	if augformtext ~= "" then
		table.insert(textsegs, ";指大詞 " .. augformtext)
	end
	table.insert(textsegs, "</span>")
	local categories = {}
	local langname = lang:getCanonicalName()
	local function insert_cats(dimaugof)
		if dimaugof == "" then
			-- No category such as "English diminutives of given names"
			table.insert(categories, langname .. "名字")
		end
		local function insert_cats_gender(g)
			if g == "性別不明" or g == "unknown-gender" then
				track("性別不明")
				return
			end
			if g ~= "男性" and g ~= "女性" and g ~= "中性" then
				error("Unrecognized gender: " .. g)
			end
			if g == "中性" then
				insert_cats_gender("男性")
				insert_cats_gender("女性")
			end
			table.insert(categories, langname .. g .. "名字" .. dimaugof)
			for _, catpart in ipairs(from_catparts) do
			table.insert(categories, "來自" .. catpart .. "的" .. langname .. g .. "名字" .. dimaugof)
			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("指小詞")
	elseif numaugs > 0 then
		insert_cats("指大詞")
	end

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

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

	if parent_args.dot or parent_args.nodot then
		error("[[Template:surname]] 不再支援 dot= 和 nodot=,預設情況下不再添加句末的句點;"
				.. "如果需要,請在模板後面手動添加")
	end

	local lang_index = compat and "lang" or 1

	local params = {
		[lang_index] = { required = true, default = "und" },
		["g"] = {list = true}, -- gender(s)
		[1 + offset] = {}, -- adjective/qualifier
		["usage"] = {},
		["origin"] = {},
		["popular"] = {},
		["populartype"] = {},
		["meaning"] = { list = true },
		["meaningtype"] = {},
		["q"] = {},
		-- initial article: by default A or An (English), a or an (otherwise)
		["A"] = {},
		["sort"] = {},
		["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 },
		["blend"] = { list = true },
		["blendtype"] = {},
		["m"] = { list = true },
		["mtype"] = {},
		["f"] = { list = true },
		["ftype"] = {},
		["nocat"] = {type = "boolean"},
	}

	local args = require("Module:parameters").process(parent_args, params)
	
	local textsegs = {}
	local langcode = args[lang_index]
	local lang = m_languages.getByCode(langcode, lang_index)

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

	local adj = args[1 + offset]
	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 meaningsegs = {}
	for _, meaning in ipairs(args.meaning) do
		table.insert(meaningsegs, '"' .. meaning .. '"')
	end
	local meaningtext = m_table.serialCommaJoin(meaningsegs, {"或"})
	local eqtext = get_eqtext(args)

	table.insert(textsegs, "<span class='use-with-mention'>")

	local genders = {}
	for _, g in ipairs(args.g) do
		local origg = g
		if g == "unknown" or g == "unknown gender" or g == "性別不明" or g == "未知性別" or g == "?" then
			g = "性別不明"
		elseif g == "unisex" or g == "common gender" or g == "c" then
			g = "通性"
		elseif g == "m" then
			g = "男性"
		elseif g == "f" then
			g = "女性"
		end
		if g == "性別不明" then
			track("性別不明")
		elseif g ~= "男性" and g ~= "女性" and g ~= "通性" then
			error("無法識別的性別:" .. origg)
		end
		table.insert(genders, g)
	end

	-- If gender is supplied, it goes before the specified adjective in adj=. The only value of gender that uses "an" is
	-- "unknown-gender" (note that "unisex" wouldn't use it but in any case we map "unisex" to "common-gender"). If gender
	-- isn't supplied, look at the first letter of the value of adj= if supplied; otherwise, the article is always "a"
	-- because the word "surname" follows. Capitalize "A"/"An" if English.
	local article
	if args.A then
		article = args.A
	else
		article = #genders > 0 and genders[1] == "未知性別" and "an" or
			#genders == 0 and adj and adj or
			"a"
		if langcode == "en" then
			article = mw.getContentLanguage():ucfirst(article)
		end
	end
	-- table.insert(textsegs, article .. " ")

	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 .. (args.from[1]:find(":") and " 的" or "的"))
	end
	if #genders > 0 then
		table.insert(textsegs, table.concat(genders, "或"))
	end
	if adj then
		table.insert(textsegs, adj)
	end
	table.insert(textsegs, "[[姓氏]]")
	local need_comma = false
	if xlittext ~= "" then
		table.insert(textsegs, "," .. xlittext)
		need_comma = true
	end
	
	if meaningtext ~= "" then
		if need_comma then
			table.insert(textsegs, ",")
		end
		need_comma = true
		table.insert(textsegs, fetch_typetext("meaningtype") .. "表示" .. meaningtext)
	end
	if args.origin then
		if need_comma then
			table.insert(textsegs, ",")
		end
		need_comma = true
		table.insert(textsegs, "來自" .. args.origin)
	end
	if args.usage then
		if need_comma then
			table.insert(textsegs, ",")
		end
		need_comma = true
		table.insert(textsegs, "用於" .. args.usage)
	end
	if varoftext ~= "" then
		table.insert(textsegs, "," .. varoftext .. "的" .. fetch_typetext("varoftype") .. "變體")
	end
	if blendtext ~= "" then
		table.insert(textsegs, "," .. blendtext .. "的" .. fetch_typetext("blendtype") .. "混成詞")
	end
	if args.popular then
		table.insert(textsegs, "," .. fetch_typetext("populartype") .. "通俗" .. args.popular)
	end
	if mtext ~= "" then
		table.insert(textsegs, "," .. fetch_typetext("mtype") .. "陽性等價詞 " .. mtext)
	end
	if ftext ~= "" then
		table.insert(textsegs, "," .. fetch_typetext("ftype") .. "陰性等價詞 " .. ftext)
	end
	if eqtext ~= "" then
		table.insert(textsegs, "," .. fetch_typetext("eqtype") .. "等價於" .. eqtext)
	end
	if args.q then
		table.insert(textsegs, "," .. args.q)
	end
	if varformtext ~= "" then
		table.insert(textsegs, ";變體形式 " .. varformtext)
	end
	table.insert(textsegs, "</span>")

	local text = table.concat(textsegs, "")
	if args.nocat then
		return text
	end

	local categories = {}
	local langname = lang:getCanonicalName()
	local function insert_cats(g)
		g = g and g or ""
		table.insert(categories, langname .. g .. "姓氏")
		for _, catpart in ipairs(from_catparts) do
			table.insert(categories, "來自" .. catpart .. "的" .. langname .. g .. "姓氏")
		end
	end
	insert_cats(nil)
	local function insert_cats_gender(g)
		local origg = g
		if g == "性別不明" then
			return
		end
		if g == "通性" then
			insert_cats_gender("男性")
			insert_cats_gender("女性")
		end
		insert_cats(g)
	end
	for _, g in ipairs(genders) do
		insert_cats_gender(g)
	end

	return text .. m_utilities.format_categories(categories, lang, args.sort, nil, force_cat)
end

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

	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 = "父名" },
		["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"] = {},
		["pagename"] = {},
	}
	
	local args = require("Module:parameters").process(parent_args, params)
	local lang = m_languages.getByCode(args[1], 1)
	local sources = {}
	for _, source in ipairs(rsplit(args[2], "%s*,%s*")) do
		local sourcelang = m_languages.getByCode(source, 2, "allow etym")
		table.insert(sources, 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("無法識別的類型「" .. ty .. "」:應為" ..
					m_table.serialCommaJoin(quoted_types, {"或"}))
			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 SUBPAGENAME = args.pagename or mw.title.getCurrentTitle().subpageText
	
	local textsegs = {}
	table.insert(textsegs, "<span class='use-with-mention'>")
	local langsegs = {}
	for i, source in ipairs(sources) do
		local sourcename = source:getCanonicalName()
		local function get_source_link()
			local term_to_link = args[3][1] or SUBPAGENAME
			-- We link the language name to either the first specified name or the pagename, in the following circumstances:
			-- (1) More than one language was given along with at least one name; or
			-- (2) We're handling {{foreign name}} or {{name obor}}, and no name was given.
			-- The reason for (1) is that if more than one language was given, we want a link to the name
			-- in each language, as the name that's displayed is linked only to the first specified language.
			-- However, if only one language was given, linking the language to the name is redundant.
			-- The reason for (2) is that {{foreign name}} is often used when the name in the destination language
			-- is spelled the same as the name in the source language (e.g. [[Clinton]] or [[Obama]] in Italian),
			-- and in that case no name will be explicitly specified but we still want a link to the name in the
			-- source language. The reason we restrict this to {{foreign name}} or {{name obor}}, not to {{name translit}}
			-- or {{name respelling}}, is that {{name translit}} and {{name respelling}} ought to be used for names
			-- spelled differently in the destination language (either transliterated or respelled), so assuming the
			-- pagename is the name in the source language is wrong.
			if args[3][1] and #sources > 1 or (iargs.foreign_name or iargs.obor) and not args[3][1] then
				return m_links.language_link{
					lang = sources[i], term = term_to_link, alt = sourcename, tr = "-"
				}
			else
				return sourcename
			end
		end
		
		if i == 1 and not iargs.foreign_name 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, get_source_link())
			else
				table.insert(langsegs, sourcename)
			end
		else
			table.insert(langsegs, get_source_link())
		end
	end
	local langseg_text = m_table.serialCommaJoin(langsegs, {"或"})
	local augdim_text
	if args.dim then
		augdim_text = "[[指小]]"
	elseif args.aug then
		augdim_text = "[[指大]]"
	else
		augdim_text = ""
	end
	local nametype_text = m_table.serialCommaJoin(nametypes) .. augdim_text

	if not iargs.foreign_name then
		table.insert(textsegs, langseg_text)
		table.insert(textsegs, nametype_text)
		if maxmaxindex > 0 then
			table.insert(textsegs, " ")
		end
	else
		table.insert(textsegs, nametype_text)
		table.insert(textsegs, "於" .. langseg_text)
		if maxmaxindex > 0 then
			table.insert(textsegs, ",")
		end
	end

	local names = {}
	local embedded_comma = false

	for i = 1, maxmaxindex do
		local sc = require("Module:scripts").getByCode(args["sc"][i], true)
		
		local terminfo = {
			lang = sources[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 not iargs.foreign_name then
			linked_term = linked_term .. " 的" .. desctext
		end
		if args["eq"][i] then
			embedded_comma = true
			linked_term = linked_term .. ",等同於 " .. m_links.language_link{ lang = m_languages.getByCode("en"), term = args["eq"][i] }
		end
		table.insert(names, linked_term)
	end

	if not args.nocap then
		desctext = mw.getContentLanguage():ucfirst(desctext)
	end
	if embedded_comma then
		table.insert(textsegs, table.concat(names, ";或來自"))
	else
		table.insert(textsegs, m_table.serialCommaJoin(names, {"或"}))
	end
	table.insert(textsegs, "</span>")

	local categories = {}
	for _, nametype in ipairs(nametypes) do
		local function insert_cats(dimaugof)
			local function insert_cats_type(ty)
				if ty == "中性名字" then
					insert_cats_type("男性名字")
					insert_cats_type("女性名字")
				end
				for i, source in ipairs(sources) do
					table.insert(categories, source:getCanonicalName() .. ty .. dimaugof .. "的" .. lang:getFullName() .. "譯名")
					table.insert(categories, "派生自" .. source:getCanonicalName() .. "的" .. lang:getFullName() .. "詞")
					table.insert(categories, "源自" .. source:getCanonicalName() .. "的" .. lang:getFullName() .. "借詞")
					if iargs.obor then
						table.insert(categories, "源自" .. source:getCanonicalName() .. "的" .. lang:getFullName() .. "形譯詞")
					end
					if source:getCode() ~= source:getFullCode() then
						-- etymology language
						table.insert(categories, source:getFullName() .. ty .. dimaugof .. "的" .. lang:getFullName() .. "譯名")
					end
				end
			end
			insert_cats_type(nametype)
		end
		insert_cats("")
		if args.dim then
			insert_cats("指小詞")
		end
		if args.aug then
			insert_cats("指大詞")
		end
	end

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

return export