Bước tới nội dung

Mô đun:fr-headword

Từ điển mở Wiktionary
local export = {}
local pos_functions = {}

local force_cat = false -- for testing; if true, categories appear in non-mainspace pages
local check_missing = false -- whether to check for missing forms

local require_when_needed = require("Module:utilities/require when needed")
local m_table = require("Module:table")
local lang = require("Module:languages").getByCode("fr")
local langname = lang:getCanonicalName()

local en_utilities_module = "Module:en-utilities"
local headword_utilities_module = "Module:headword utilities"
local romut_module = "Module:romance utilities"

local m_en_utilities = require_when_needed(en_utilities_module)
local m_headword_utilities = require_when_needed(headword_utilities_module)
local glossary_link = require_when_needed(headword_utilities_module, "glossary_link")

local boolean_param = {type = "boolean"}
local list_param = {list = true, disallow_holes = true}

local concat = table.concat
local insert = table.insert
local remove = table.remove
local sort = table.sort
local u = mw.ustring.char
local nnbsp = u(0x202F) -- NNBSP = narrow non-breaking space, used before ? and ! in French

local prepositions = {
	"à ",
	"aux? ",
	"d[eu] ",
	"d['’]",
	"des ",
	"en ",
	"sous ",
	"sur ",
	"avec ",
	"pour ",
	"par ",
	"dans ",
	"contre ",
	"sans ",
	"comme ",
	"jusqu['’]",
	-- We could list others but you get diminishing returns
}

local no_split_apostrophe_words = {
	["c'est"] = true,
	["quelqu'un"] = true,
	["aujourd'hui"] = true,
}

local function track(page)
	require("Module:debug").track("fr-headword/" .. page)
	return true
end


-----------------------------------------------------------------------------------------
--                                     Utility functions                               --
-----------------------------------------------------------------------------------------

local function replace_hash_with_lemma(term, lemma)
	-- If there is a % sign in the lemma, we have to replace it with %% so it doesn't get interpreted as a capture replace
	-- expression.
	lemma = lemma:gsub("%%", "%%%%")
	-- Assign to a variable to discard second return value.
	term = term:gsub("#", lemma)
	return term
end

-- Parse and insert an inflection not requiring additional processing into `data.inflections`. The raw arguments come
-- from `args[field]`, which is parsed for inline modifiers. `label` is the label that the inflections are given;
-- `accel` is the accelerator form, or nil.
local function parse_and_insert_inflection(data, args, field, label, accel)
	m_headword_utilities.parse_and_insert_inflection {
		headdata = data,
		forms = args[field],
		paramname = field,
		splitchar = ",",
		label = label,
		accel = accel and {form = accel} or nil,
	}
end

local function make_plural(form, special)
	local retval = require(romut_module).handle_multiword(form, special, make_plural, prepositions)
	if retval then
		if #retval > 1 then
			error("Lỗi nội bộ: Got multiple plurals from handle_multiword(): " .. concat(retval))
		end
		return retval[1]
	end

	if form:match("[sxz]$") then
		return form
	elseif form:match("au$") then
		return form .. "x"
	elseif form:match("al$") then
		return form:gsub("al$", "aux")
	end
	return form .. "s"
end

local function make_feminine(form, special)
	local retval = require(romut_module).handle_multiword(form, special, make_feminine, prepositions)
	if retval then
		if #retval > 1 then
			error("Lỗi nội bộ: Got multiple feminines from handle_multiword(): " .. concat(retval))
		end
		return retval[1]
	end

	if form:match("e$") then
		return form
	elseif form:match("en$") then
		return form .. "ne"
	elseif form:match("er$") then
		return form:gsub("er$", "ère")
	elseif form:match("el$") then
		return form .. "le"
	elseif form:match("et$") then
		return form .. "te"
	elseif form:match("on$") then
		return form .. "ne"
	elseif form:match("ieur$") then
		return form .. "e"
	elseif form:match("teur$") then
		return form:gsub("teur$", "trice")
	elseif form:match("eu[rx]$") then
		return form:gsub("eu[rx]$", "euse")
	elseif form:match("if$") then
		return form:gsub("if$", "ive")
	elseif form:match("c$") then
		return form:gsub("c$", "que")
	elseif form:match("eau$") then
		return form:gsub("eau$", "elle")
	else
		return form .. "e"
	end
end

-- For bot use
function export.make_feminine(frame)
	local masc = frame.args[1] or error("giống đực trong tham số đầu tiên bắt buộc phải có.")
	local special = frame.args[2]
	return make_feminine(masc, special)
end

-- Handle generation of feminine or plural inflections when "+" is encountered. `list` is the existing base forms
-- (strings or form objects) that need to be inflected. `defobj` is the form object whose `term` field is "+"; any
-- qualifiers, labels and/or references from that form object will be combined with those of the base form. `inflfun`
-- is the function to do the inflection, which is passed two arguments, the base form value (a string) and the
-- `special` value specified for handling multiword expressions (or nil if no value specified). Returns the new list
-- of inflected form objects.
local function make_inflection(list, defobj, inflfun, special)
	local newlist = {}
	for _, form in ipairs(list) do
		if type(form) == "string" then
			form = {term = form}
		end
		local infl = inflfun(form.term, special)
		local formobj = m_table.shallowCopy(form)
		formobj.term = infl
		m_headword_utilities.combine_termobj_qualifiers_labels(formobj, defobj)
		insert(newlist, formobj)
	end
	return newlist
end


-----------------------------------------------------------------------------------------
--                                    Main entry point                                 --
-----------------------------------------------------------------------------------------

-- The main entry point.
-- This is the only function that can be invoked from a template.
function export.show(frame)
	local poscat = frame.args[1] or error("Từ loại không được chỉ rõ. Vui lòng thêm tham số đầu tiên để gọi mô đun.")

	local params = {
		["head"] = list_param,
		["id"] = true,
		["json"] = boolean_param,
		["splithyph"] = boolean_param,
		["nolink"] = boolean_param,
		["nolinkhead"] = {type = "boolean", alias_of = "nolink"},
		["abbr"] = list_param,
		["pagename"] = true, -- for testing
	}

	if pos_functions[poscat] then
		for key, val in pairs(pos_functions[poscat].params) do
			params[key] = val
		end
	end

	local parargs = frame:getParent().args
	local args = require("Module:parameters").process(parargs, params)

	local pagename = args.pagename or mw.loadData("Module:headword/data").pagename

	local user_specified_heads = args.head
	local heads = user_specified_heads
	
	if args.nolink then
		if not heads[1] then
			heads = {pagename}
		end
	else
		local romut = require(romut_module)
		local auto_linked_head = romut.add_links_to_multiword_term(pagename, args.splithyph, no_split_apostrophe_words)
		if not heads[1] then
			heads = {auto_linked_head}
		else
			for i, head in ipairs(heads) do
				if head:find("^~") then
					head = romut.apply_link_modifiers(auto_linked_head, head:sub(2))
					heads[i] = head
				elseif head:find("^[!?]$") then
					-- If explicit head= just consists of ! or ?, add it to the end of the default head after a
					-- NNBSP (thin non-breaking space).
					head = auto_linked_head .. nnbsp .. head
					heads[i] = head
				end
				if head == auto_linked_head then
					track("redundant-head")
				end
			end
		end
	end

	local data = {
		lang = lang,
		pos_category = pos_functions[poscat] and pos_functions[poscat].pos_category or poscat,
		categories = {},
		heads = heads,
		user_specified_heads = user_specified_heads,
		no_redundant_head_cat = not user_specified_heads[1],
		genders = {},
		inflections = {},
		pagename = pagename,
		id = args.id,
		checkredlinks = check_missing and (pos_functions[poscat] and pos_functions[poscat].redlink_pos or true) or false,
	}

	if pagename:find("^%-") and poscat ~= "Biến thể hình thái hậu tố" then
		data.is_suffix = true
		data.pos_category = "Hậu tố"
		data.checkredlinks = check_missing
		local singular_poscat = require(en_utilities_module).singularize(poscat)
		insert(data.categories, "Dạng hậu tố " .. singular_poscat .. " " .. langname)
		insert(data.inflections, {label = "dạng hậu tố " .. singular_poscat})
	end

	if pos_functions[poscat] then
		pos_functions[poscat].func(args, data)
	end

	parse_and_insert_inflection(data, args, "abbr", "từ viết tắt")
	
	if args.json then
		return require("Module:JSON").toJSON(data)
	end

	return require("Module:headword").full_headword(data)
end


-----------------------------------------------------------------------------------------
--                                          Nouns                                      --
-----------------------------------------------------------------------------------------

local allowed_genders = m_table.listToSet {
	"m", "f", "mf", "mfbysense", "mfequiv", "gneut", "m-p", "f-p", "mf-p", "mfbysense-p", "mfequiv-p", "gneut-p",
	"?", "?-p",
}

local additional_allowed_pronoun_genders = m_table.listToSet {
	"m-s", "f-s", "mf-s",
	"p", -- mf-p doesn't make sense for e.g. [[iels]]/[[ielles]]
	"n", -- e.g. [[ceci]]/[[cela]]
}

local function validate_genders(genders, is_pronoun)
	for _, g in ipairs(genders) do
		if type(g) == "table" then
			g = g.spec
		end
		if not allowed_genders[g] and (not is_pronoun or not additional_allowed_pronoun_genders[g]) then
			error("Không rõ giống: " .. g)
		end
	end
end

local function make_qual_replaced_by(replacement)
	return {list = true, allow_holes = true, replaced_by = false,
		instead = ("use an inline modifier on |%s= such as <q:...>, <qq:...>, <l:...> or <ll:...>"):format(replacement)
	}
end

local function make_alias_replaced_by(replacement, reason)
	return {list = true, disallow_holes = true, replaced_by = replacement, reason = reason or
		"for consistency with the corresponding parameter in other Romance-language headword templates"
	}
end

local function get_noun_params(is_proper)
	return {
		[1] = {list = "g", disallow_holes = true, required = not is_proper, default = "?", type = "genders",
			flatten = true}, -- gender(s)
		["g"] = {replaced_by = 1, reason = "for consistency"},
		[2] = list_param, --plural override(s)
		["pqual"] = make_qual_replaced_by("2"),
		["f"] = list_param, --feminine form(s)
		["fqual"] = make_qual_replaced_by("f"),
		["m"] = list_param, --masculine form(s)
		["fqual"] = make_qual_replaced_by("m"),
		["dim"] = list_param, --diminutive(s)
		["dimqual"] = make_qual_replaced_by("dim"),
		["aug"] = list_param, --diminutive(s)
		["pej"] = list_param, --pejorative(s)
		["dem"] = list_param, --demonym(s)
		["fdem"] = list_param, --female demonym(s)
	}
end

local function do_noun(args, data, pos, is_proper)
	local is_plurale_tantum = false
	local has_singular = false
	if pos == "Danh từ số đếm" then
		pos = "Số từ"
		insert(data.categories, 1, "Số đếm " .. langname)
	end
	if data.is_suffix then
		pos = "Hậu tố"
	end
	local plpos = pos

	validate_genders(args[1])
	data.genders = args[1]
	-- Check for specific genders and pluralia tantum.
	for _, g in ipairs(args[1]) do
		if type(g) == "table" then
			g = g.spec
		end
		if g:find("-p$") then
			is_plurale_tantum = true
		else
			has_singular = true
		end
	end

	local lemma = data.pagename

	-- Plural
	local plurals = {}

	local function insert_noun_inflection(terms, label, accel)
		m_headword_utilities.insert_inflection {
			headdata = data,
			terms = terms,
			label = label,
			accel = accel and {form = accel} or nil,
		}
	end

	if is_plurale_tantum and not has_singular then
		if args[2][1] then
			error("Can't specify plurals of plurale tantum " .. pos)
		end
		insert(data.inflections, {label = glossary_link("chỉ có số nhiều")})
	else
		plurals = m_headword_utilities.parse_term_list_with_modifiers {
			paramname = {2, "pl"},
			forms = args[2],
			splitchar = ",",
		}
		-- Check for special plural signals
		local mode = nil

		local pl1 = plurals[1]
		if pl1 and #pl1.term == 1 then
			mode = pl1.term
			if mode == "?" or mode == "!" or mode == "-" or mode == "~" then
				pl1.term = nil
				if next(pl1) then
					error(("Can't specify inline modifiers with plural code '%s'"):format(mode))
				end
				table.remove(plurals, 1)  -- Remove the mode parameter
			elseif mode ~= "+" and mode ~= "#" and not mode:find("[a-zA-Z]") then
				error(("Unexpected plural code '%s'"):format(mode))
			end
		end

		if is_plurale_tantum then
			-- both singular and plural
			insert(data.inflections, {label = "đôi khi " .. glossary_link("chỉ có số nhiều") .. ", trong biến thể"})
		end
		if mode == "?" then
			-- Plural is unknown
			insert(data.categories, plpos .. " số nhiều chưa được chứng thực hoặc không rõ " .. langname)
		elseif mode == "!" then
			-- Plural is not attested
			insert(data.inflections, {label = "số nhiều chưa được chứng thực"})
			insert(data.categories, plpos .. " số nhiều chưa được chứng thực " .. langname)
			if plurals[1] then
				error("Can't specify any plurals along with unattested plural code '!'")
			end
		elseif mode == "-" then
			-- Uncountable noun; may occasionally have a plural
			insert(data.categories, plpos .. " không đếm được " .. langname)

			-- If plural forms were given explicitly, then show "usually"
			if plurals[1] then
				insert(data.inflections, {label = "thường " .. glossary_link("không đếm được")})
				insert(data.categories, plpos .. " đếm được " .. langname)
			else
				insert(data.inflections, {label = glossary_link("không đếm được")})
			end
		else
			-- Countable or mixed countable/uncountable
			if not plurals[1] and not is_proper then
				plurals[1] = {term = "+"}
			end
			if mode == "~" then
				-- Mixed countable/uncountable noun, always has a plural
				insert(data.inflections, {label = glossary_link("đếm được") .. " và " .. glossary_link("không đếm được")})
				insert(data.categories, plpos .. " không " .. langname)
				insert(data.categories, plpos .. " đếm được " .. langname)
			elseif plurals[1] then
				-- Countable nouns
				insert(data.categories, plpos .. " đếm được " .. langname)
			else
				-- Uncountable nouns
				insert(data.categories, plpos .. " không đếm được " .. langname)
			end
		end

		-- Gather plurals, handling requests for default plurals.
		for _, pl in ipairs(plurals) do
			if pl.term == "+" then
				pl.term = make_plural(lemma)
			elseif pl.term:find("^%+") then
				pl.term = require(romut_module).get_special_indicator(pl.term)
				pl.term = make_plural(lemma, pl.term)
			elseif pl.term == "s" or pl.term == "x" then
				-- FIXME, convert to #s or #x
				pl.term = lemma .. pl.term
			else
				pl.term = replace_hash_with_lemma(pl.term, lemma)
			end
		end

		local pl1 = plurals[1]
		if pl1 and not plurals[2] and pl1.term == lemma then
			insert(data.inflections, {label = glossary_link("không biến cách"),
				q = pl1.q, qq = pl1.qq, l = pl1.l, ll = pl1.ll, refs = pl1.refs
			})
			insert(data.categories, plpos .. " không biến cách " .. langname)
		else
			insert_noun_inflection(plurals, "số nhiều", "p")
		end

		if plurals[2] then
			insert(data.categories, plpos .. " với nhiều dạng số nhiều " .. langname)
		end
	end

	-- Gather masculines/feminines. For each one, generate the corresponding plural. `field` is the name of the field
	-- containing the masculine or feminine forms (normally "m" or "f"); and `inflect` is a function of one or two
	-- arguments to generate the default masculine or feminine from the lemma (the arguments are the lemma and
	-- optionally a "special" flag to indicate how to handle multiword lemmas, and the function is normally
	-- make_feminine or make_masculine [which doesn't exist, FIXME]).
	local function handle_mf(field, inflect, default_plurals)
		local function call_inflect(special)
			if inflect then
				-- Generate default feminine.
				return inflect(lemma, special)
			else
				-- FIXME
				error("Can't generate default masculine currently")
			end
		end

		local mfs = m_headword_utilities.parse_term_list_with_modifiers {
			paramname = field,
			forms = args[field],
			splitchar = ",",
			frob = function(term)
				if term == "+" then
					-- Generate default masculine/feminine.
					term = call_inflect()
				elseif term == "e" and field == "f" then
					-- FIXME: remove this special case
					term = lemma .. "e"
				else
					term = replace_hash_with_lemma(term, lemma)
				end
				local special = require(romut_module).get_special_indicator(term)
				if special then
					term = call_inflect(special)
				end
				return term
			end
		}
		return mfs
	end

	local feminines = handle_mf("f", make_feminine)
	local masculine_plurals = {}
	local masculines = handle_mf("m", nil)

	local function parse_and_insert_noun_inflection(field, label, accel)
		parse_and_insert_inflection(data, args, field, label, accel)
	end

	insert_noun_inflection(feminines, "giống đực", "f")
	insert_noun_inflection(masculines, "giống cái")

	parse_and_insert_noun_inflection("dim", "giảm nhẹ nghĩa")
	parse_and_insert_noun_inflection("aug", "tăng kích")
	parse_and_insert_noun_inflection("pej", "nghĩa xấu")
	parse_and_insert_noun_inflection("dem", "tên gọi dân cư")
	parse_and_insert_noun_inflection("fdem", "tên gọi dân cư dành cho nữ")
end

pos_functions["Danh từ"] = {
	params = get_noun_params(),
	func = function(args, data)
		do_noun(args, data, "Danh từ")
	end,
}

pos_functions["Danh từ riêng"] = {
	params = get_noun_params("is proper"),
	func = function(args, data)
		do_noun(args, data, "Danh từ", "is proper")
	end,
}

pos_functions["Danh từ số đếm"] = {
	params = get_noun_params(),
	func = function(args, data)
		do_noun(args, data, "Danh từ số đếm")
	end,
	pos_category = "Số từ",
}

local function get_pronoun_pos(pos)
	return {
		params = {
			[1] = {list = "g", disallow_holes = true, required = true, default = "?", type = "genders",
				flatten = true}, -- gender(s)
			["g"] = {replaced_by = 1, reason = "for consistency"},
			["f"] = list_param,
			["fqual"] = make_qual_replaced_by("f"),
			["m"] = list_param,
			["mqual"] = make_qual_replaced_by("m"),
			["mv"] = list_param,
			["mvqual"] = make_qual_replaced_by("mv"),
			["fpl"] = list_param,
			["fp"] = make_alias_replaced_by("fpl"),
			["fpqual"] = make_qual_replaced_by("fpl"),
			["mpl"] = list_param,
			["mp"] = make_alias_replaced_by("mpl"),
			["mpqual"] = make_qual_replaced_by("mpl"),
			["pl"] = list_param,
			["p"] = make_alias_replaced_by("pl"),
			["pqual"] = make_qual_replaced_by("pl"),
			["type"] = list_param,
		},

		func = function(args, data)
			-- Validate and add genders.
			validate_genders(args[1], "is pronoun")
			data.genders = args[1]

			local plpos = pos

			local function parse_and_insert_pronoun_inflection(field, label, accel)
				parse_and_insert_inflection(data, args, field, label, accel)
			end

			-- Parse and insert all inflections.
			parse_and_insert_pronoun_inflection("m", "giống đực")
			parse_and_insert_pronoun_inflection("mv", "nguyên âm trước ở giống đực số ít")
			parse_and_insert_pronoun_inflection("f", "giống cái")
			parse_and_insert_pronoun_inflection("mpl", "giống đực số nhiều")
			parse_and_insert_pronoun_inflection("fpl", "giống cái số nhiều")
			parse_and_insert_pronoun_inflection("pl", "số nhiều")

			-- Categorize by "type"
			for _, ty in ipairs(args.type) do
				local category
				if ty == "indefinite" then
					category = "bất định"
				elseif ty == "interrogative" then
					category = "nghi vấn"
				elseif ty == "personal" then
					category = "nhân xưng"
				elseif ty == "possessive" then
					category = "sở hữu"
				elseif ty == "reflexive" then
					category = "phản thân"
				elseif ty == "relative" then
					category = "quan hệ"
				end
				if category then
					if type(category) == "table" then
						for _, cat in ipairs(category) do
							insert(data.categories, "Đại từ " .. cat .. " " .. langname)
						end
					else
						insert(data.categories, langname .. " " .. category .. " pronouns")
					end
				end
			end
		end
	}
end

pos_functions["Đại từ"] = get_pronoun_pos("Đại từ")
pos_functions["Từ hạn định"] = get_pronoun_pos("Từ hạn định")

local function get_misc_pos()
	return {
		params = {
			[1] = {replaced_by = "head", reason = "for consistency with other French headword templates"},
		},

		func = function(args, data)
		end
	}
end

pos_functions["Phó từ"] = get_misc_pos()

pos_functions["Giới từ"] = get_misc_pos()

pos_functions["Cụm từ"] = get_misc_pos()

pos_functions["Cụm giới từ"] = get_misc_pos()

pos_functions["Tục ngữ"] = get_misc_pos()

pos_functions["Dấu câu"] = get_misc_pos()

pos_functions["Dấu phụ"] = get_misc_pos()

pos_functions["Thán từ"] = get_misc_pos()

pos_functions["Tiền tố"] = get_misc_pos()

pos_functions["Từ viết tắt"] = get_misc_pos()

local function do_adjective(pos)
	return {
		params = {
			[1] = true,
			["inv"] = boolean_param,
			["sp"] = true, -- special indicator: "first", "first-last", etc.
			["onlyg"] = true,
			["m"] = list_param,
			["mqual"] = make_qual_replaced_by("m"),
			["mv"] = list_param,
			["mvqual"] = make_qual_replaced_by("mv"),
			["f"] = list_param,
			["fqual"] = make_qual_replaced_by("f"),
			["mpl"] = list_param,
			["mp"] = make_alias_replaced_by("mpl"),
			["mpqual"] = make_qual_replaced_by("mpl"),
			["fpl"] = list_param,
			["fp"] = make_alias_replaced_by("fpl"),
			["fpqual"] = make_qual_replaced_by("fpl"),
			["pl"] = list_param,
			["p"] = make_alias_replaced_by("pl"),
			["pqual"] = make_qual_replaced_by("pl"),
			["base"] = list_param,
			["current"] = make_alias_replaced_by("base", "because the old name was obscure and did not clarify the purpose of the parameter"),
			["comp"] = list_param,
			["compqual"] = make_qual_replaced_by("comp"),
			["sup"] = list_param,
			["supqual"] = make_qual_replaced_by("sup"),
			["intr"] = boolean_param,
		},

		pos_category = pos == "Tính từ số đếm" and "Số từ" or nil,
		redlink_pos = pos == "Phân từ" and "Phân từ" or nil,

		func = function(args, data)
			local is_numeral = false
			if pos == "Tính từ số đếm" then
				pos = "Số từ"
				is_numeral = true
				insert(data.categories, 1, "Số đếm " .. langname)
			end
			if data.is_suffix then
				pos = "Hậu tố"
			end
			local plpos = pos

			if not is_numeral then
				if args.onlyg == "p" or args.onlyg == "m-p" or args.onlyg == "f-p" then
					insert(data.categories, "Danh từ chỉ có số nhiều " .. langname)
				end
				if args.onlyg == "s" or args.onlyg == "f-s" or args.onlyg == "f-s" then
					insert(data.categories, "Danh từ chỉ có số ít " .. langname)
				end
				if args.onlyg then
					insert(data.categories, plpos .. " khiếm khuyết " .. langname)
				end
			end

			local lemma = data.pagename

			local function insert_inflection(terms, label, accel)
				m_headword_utilities.insert_inflection {
					headdata = data,
					terms = terms,
					label = label,
					accel = accel and {form = accel} or nil,
				}
			end

			local function process_inflection(label, arg, accel, get_default, explicit_default_only)
				local orig_infls = m_headword_utilities.parse_term_list_with_modifiers {
					paramname = arg,
					forms = args[arg],
					splitchar = ",",
				}
				if not orig_infls[1] then
					if explicit_default_only or not get_default then
						orig_infls = {}
					else
						orig_infls = {{term = "+"}}
					end
				end
				local infls = {}
				if orig_infls[1] then
					for _, infl in ipairs(orig_infls) do
						if infl.term == "+" then
							local defs
							if get_default then
								defs = get_default(infl)
							else
								error("Can't use '+' with " .. arg .. "=; no default available")
							end
							for _, def in ipairs(defs) do
								insert(infls, def)
							end
						elseif infl.term == "e" or infl.term == "s" or infl.term == "x" then
							-- FIXME: delete this in favor of #e, #s, #x
							infl.term = lemma .. infl.term
							insert(infls, infl)
						else
							infl.term = replace_hash_with_lemma(infl.term, lemma)
							insert(infls, infl)
						end
					end
					insert_inflection(infls, label, accel)
				end
				return infls
			end

			if args.sp and not require(romut_module).allowed_special_indicators[args.sp] then
				local indicators = {}
				for indic, _ in pairs(require(romut_module).allowed_special_indicators) do
					insert(indicators, "'" .. indic .. "'")
				end
				sort(indicators)
				error("Special inflection indicator beginning can only be " ..
					mw.text.listToText(indicators) .. ": " .. args.sp)
			end

			local function get_base()
				return args.base[1] and args.base or {lemma}
			end

			if args.onlyg == "p" then
				insert(data.inflections, {label = "chỉ có số nhiều"})
				if args[1] ~= "mf" then
					-- Handle feminine plurals
					process_inflection("giống cái số nhiều", "fpl", "f|p")
				end
			elseif args.onlyg == "s" then
				insert(data.inflections, {label = "chỉ có số ít"})
				if not (args[1] == "mf" or not args.f[1] and lemma:match("e$")) then
					-- Handle feminines
					process_inflection("giống cái số ít", "f", "f", function(defobj)
						return make_inflection(get_base(), defobj, make_feminine, args.sp)
					end)
				end
			elseif args.onlyg == "m" then
				insert(data.genders, "m")
				insert(data.inflections, {label = "chỉ có giống đực"})
				-- Handle masculine plurals
				process_inflection("giống đực số nhiều", "mpl", "m|p", function(defobj)
					return make_inflection(get_base(), defobj, make_plural, args.sp)
				end)
			elseif args.onlyg == "f" then
				insert(data.genders, "f")
				insert(data.inflections, {label = "chỉ có giống cái"})
				-- Handle feminine plurals
				process_inflection("giống cái số nhiều", "fpl", "f|p", function(defobj)
					return make_inflection(get_base(), defobj, make_plural, args.sp)
				end)
			elseif args.onlyg then
				insert(data.genders, args.onlyg)
				insert(data.inflections, {label = "khiếm khuyết"})
			else
				-- Gather genders
				local gender = args[1]
				-- Default to mf if base form ends in -e and no feminine,
				-- feminine plural or gender specified
				if not gender and not args.f[1] and not args.fpl[1] and lemma:match("e$")
					and not lemma:find(" ", nil, true) then
					gender = "mf"
				end

				if args.intr then
					insert(data.inflections, {label = glossary_link("nội động từ")})
					insert(data.inflections, {label = glossary_link("không biến cách")})
					args.inv = true
				elseif args.inv then
					insert(data.inflections, {label = glossary_link("không biến cách")})
				end

				-- Handle plurals of mf adjectives
				if not args.inv and gender == "mf" then
					process_inflection("số nhiều", "pl", "p", function(defobj)
						return make_inflection(get_base(), defobj, make_plural, args.sp)
					end)
				end

				if not args.inv and gender ~= "mf" then
					-- Handle masculine form if not same as lemma; e.g. [[sûr de soi]] with m=+, m2=sûr de lui
					process_inflection("giống đực số ít", "m", "m|s",
						function(defobj)
							defobj = m_table.shallowCopy(defobj)
							defobj.term = lemma
							return {defobj}
						end, "explicit default only")

					-- Handle case of special masculine singular before vowel
					process_inflection("nguyên âm trước ở giống đực số ít", "mv", "m|s")

					-- Handle feminines
					local feminines = process_inflection("giống cái", "f", "f|s", function(defobj)
						return make_inflection(get_base(), defobj, make_feminine, args.sp)
					end)

					-- Handle masculine plurals
					process_inflection("giống đực số nhiều", "mpl", "m|p", function(defobj)
						return make_inflection(get_base(), defobj, make_plural, args.sp)
					end)

					-- Handle feminine plurals
					process_inflection("giống cái số nhiều", "fpl", "f|p", function(defobj)
						return make_inflection(feminines, defobj, make_plural, args.sp)
					end)
				end
			end

			-- Handle comparatives
			process_inflection("so sánh hơn", "comp", "so sánh hơn")

			-- Handle superlatives
			process_inflection("so sánh nhất", "sup", "so sánh nhất")
		end
	}
end

pos_functions["Tính từ"] = do_adjective("Tính từ")
pos_functions["Phân từ quá khứ"] = do_adjective("Phân từ")
pos_functions["Tính từ số đếm"] = do_adjective("cardinal adjective")

pos_functions["Động từ"] = {
	params = {
		["type"] = list_param,
	},

	func = function(args, data)
		local pos = "Động từ"
		for _, ty in ipairs(args.type) do
			local category, label
			if ty == "auxiliary" then
				category = "trợ động từ"
			elseif ty == "defective" then
				category = "khiếm khuyết"
				label = glossary_link("defective")
			elseif ty == "impersonal" then
				category = "không ngôi"
				label = glossary_link("impersonal")
			elseif ty == "modal" then
				category = "khiếm khuyết"
			elseif ty == "reflexive" then
				category = "phản thân"
			elseif ty == "transitive" then
				label = glossary_link("ngoại động từ")
				category = "goại động từ"
			elseif ty == "intransitive" then
				label = glossary_link("nội động từ")
				category = "nội động từ"
			elseif ty == "ambitransitive" or ty == "ambi" then
				category = {"ngoại động từ", "nội động từ"}
				label = glossary_link("ngoại động từ") .. " và " .. glossary_link("nội động từ")
			end
			if category then
				if type(category) == "table" then
					for _, cat in ipairs(category) do
						insert(data.categories, pos .. " " .. cat .. " " .. langname)
					end
				else
					insert(data.categories, pos .. " " .. category .. " " .. langname)
				end
			end
			if label then
				insert(data.inflections, {label = label})
			end
		end
	end
}

pos_functions["Số đếm không biến cách"] = {
	params = {},
	func = function(args, data)
		data.pos_category = "Số từ"
		insert(data.categories, "Số đếm " .. langname)
		insert(data.categories, "Số đếm không biến cách" .. langname)
		insert(data.inflections, {label = glossary_link("không biến cách")})
	end
}


-----------------------------------------------------------------------------------------
--                                    Suffix forms                                     --
-----------------------------------------------------------------------------------------

pos_functions["Biến thể hình thái hậu tố"] = {
	params = {
		[1] = {required = true, list = true, disallow_holes = true},
		["g"] = {list = true, disallow_holes = true, type = "genders", flatten = true},
	},
	func = function(args, data)
		validate_genders(args.g)
		data.genders = args.g
		local suffix_type = {}
		for _, typ in ipairs(args[1]) do
			insert(suffix_type, "dang hậu tố " .. typ)
		end
		insert(data.inflections, {label = "dạng biến thể hình thái của " .. m_table.serialCommaJoin(suffix_type, {conj = "hoặc"})})
	end,
}

return export