Module:pl-IPA

Definition from Wiktionary, the free dictionary
Jump to navigation Jump to search

This module is used by {{pl-IPA}} to generate IPA pronunciations of Polish words.

See also: Module:pl-IPA/testcases.


local export = {}

local m_IPA = require("Module:IPA")

local lang = require("Module:languages").getByCode("pl")

local letters2phones = {
	["a"] = {
		["u"] = { "a", "w" },
		[false] = "a",
	},
	["ą"] = {
		["ł"] = { "ɔ", "w" },
		[false] = "ɔ̃",
	},
	["b"] = {
		["i"] = {
			["a"] = { "bʲ", "j", "a" },
			["ą"] = { "bʲ", "j", "ɔ̃" },
			["e"] = { "bʲ", "j", "ɛ" },
			["ę"] = { "bʲ", "j", "ɛ̃" },
			["i"] = { "bʲ", "i" },
			["o"] = { "bʲ", "j", "ɔ" },
			["ó"] = { "bʲ", "j", "u" },
			["u"] = { "bʲ", "j", "u" },
			[false] = { "bʲ", "i" }
		},
		[false] = "b"
	},
	["c"] = {
		["i"] = {
			["ą"] = { "t͡ɕ", "ɔ̃" },
			["a"] = { "t͡ɕ", "a" },
			["e"] = { "t͡ɕ", "ɛ" },
			["ę"] = { "t͡ɕ", "ɛ̃" },
			["o"] = { "t͡ɕ", "ɔ" },
			["ó"] = { "t͡ɕ", "u" },
			["u"] = { "t͡ɕ", "u" },
			["y"] = { "t͡ɕ", "ɨ" },
			[false] = { "t͡ɕ", "i" }
		},
		["h"] = {
			["i"] = {
				["a"] = { "xʲ", "j", "a" },
				["ą"] = { "xʲ", "j", "ɔ̃" },
				["e"] = { "xʲ", "j", "ɛ" },
				["ę"] = { "xʲ", "j", "ɛ̃" },
				["i"] = { "xʲ", "j", "i" },
				["o"] = { "xʲ", "j", "ɔ" },
				["ó"] = { "xʲ", "j", "u" },
				["u"] = { "xʲ", "j", "u" },
				[false] = { "xʲ", "i" }
			},
			[false] = "x"
		},
		["z"] = "t͡ʂ",
		[false] = "t͡s"
	},
	["ć"] = "t͡ɕ",
	["d"] = {
		["z"] = {
			["i"] = {
				["ą"] = { "d͡ʑ", "ɔ̃" },
				["a"] = { "d͡ʑ", "a" },
				["e"] = { "d͡ʑ", "ɛ" },
				["ę"] = { "d͡ʑ", "ɛ̃" },
				["o"] = { "d͡ʑ", "ɔ" },
				["ó"] = { "d͡ʑ", "u" },
				["u"] = { "d͡ʑ", "u" },
				["y"] = { "d͡ʑ", "ɨ" },
				[false] = { "d͡ʑ", "i" }
			},
			[false] = "d͡z"
		},
		["ż"] = "d͡ʐ",
		["ź"] = "d͡ʑ",
		[false] = "d"
	},
	["e"] = {
		["u"] = { "ɛ", "w" },
		["e"] = { "ɛ", "ʔ", "ɛ" }, -- reedukacja, reewaluacja, etc.
		[false] = "ɛ",
	},
	["ę"] = {
		["l"] = { "ɛ", "l" },
		["ł"] = { "ɛ", "w" },
		[false] = "ɛ̃",
	},
	["f"] = {
		["i"] = {
			["a"] = { "fʲ", "j", "a" },
			["ą"] = { "fʲ", "j", "ɔ̃" },
			["e"] = { "fʲ", "j", "ɛ" },
			["ę"] = { "fʲ", "j", "ɛ̃" },
			["i"] = { "fʲ", "j", "i" },
			["o"] = { "fʲ", "j", "ɔ" },
			["ó"] = { "fʲ", "j", "u" },
			["u"] = { "fʲ", "j", "u" },
			[false] = { "fʲ", "i" }
		},
		[false] = "f"
	},
	["g"] = {
		["i"] = {
			["a"] = { "ɡʲ", "j", "a" },
			["ą"] = { "ɡʲ", "j", "ɔ̃" }, -- only forms of "giąć"
			["e"] = { "ɡʲ", "j", "ɛ" },
			["ę"] = { "ɡʲ", "j", "ɛ̃" }, -- only forms of "giąć" and "giętki"
			["i"] = { "ɡʲ", "j", "i" },
			["o"] = { "ɡʲ", "j", "ɔ" },
			["ó"] = { "ɡʲ", "j", "u" },
			["u"] = { "ɡʲ", "j", "u" },
			[false] = { "ɡʲ", "i" }
		},
		[false] = "ɡ"
	},
	["h"] = {
		["i"] = {
			["a"] = { "xʲ", "j", "a" },
			["ą"] = { "xʲ", "j", "ɔ̃" },
			["e"] = { "xʲ", "j", "ɛ" },
			["ę"] = { "xʲ", "j", "ɛ̃" },
			["i"] = { "xʲ", "j", "i" },
			["o"] = { "xʲ", "j", "ɔ" },
			["ó"] = { "xʲ", "j", "u" },
			["u"] = { "xʲ", "j", "u" },
			[false] = { "xʲ", "i" }
		},
		[false] = "x"
	},
	["i"] = "i",
	["j"] = "j",
	["k"] = {
		["i"] = {
			["a"] = { "kʲ", "j", "a" },
			["ą"] = { "kʲ", "j", "ɔ̃" },
			["e"] = { "kʲ", "j", "ɛ" },
			["ę"] = { "kʲ", "j", "ɛ̃" },
			["i"] = { "kʲ", "j", "i" },
			["o"] = { "kʲ", "j", "ɔ" },
			["ó"] = { "kʲ", "j", "u" },
			["u"] = { "kʲ", "j", "u" },
			[false] = { "kʲ", "i" }
		},
		[false] = "k"
	},
	["l"] = {
		["i"] = {
			["a"] = { "lʲ", "a" },
			["ą"] = { "lʲ", "ɔ̃" },
			["e"] = { "lʲ", "ɛ" },
			["ę"] = { "lʲ", "ɛ̃" },
			["i"] = { "lʲ", "j", "i" },
			["o"] = { "lʲ", "ɔ" },
			["ó"] = { "lʲ", "u" },
			["u"] = { "lʲ", "u" },
			[false] = { "lʲ", "i" }
		},
		[false] = "l"
	},
	["ł"] = "w",
	["m"] = {
		["i"] = {
			["a"] = { "mʲ", "j", "a" },
			["ą"] = { "mʲ", "j", "ɔ̃" },
			["e"] = { "mʲ", "j", "ɛ" },
			["ę"] = { "mʲ", "j", "ɛ̃" },
			["i"] = { "mʲ", "j", "i" },
			["o"] = { "mʲ", "j", "ɔ" },
			["ó"] = { "mʲ", "j", "u" },
			["u"] = { "mʲ", "j", "u" },
			[false] = { "mʲ", "i" }
		},
		[false] = "m"
	},
	["n"] = {
		["k"] = { "ŋ", "k" },
		["g"] = { "ŋ", "ɡ" },
		["i"] = {
			["ą"] = { "ɲ", "ɔ̃" },
			["a"] = { "ɲ", "a" },
			["e"] = { "ɲ", "ɛ" },
			["ę"] = { "ɲ", "ɛ̃" },
			["i"] = { "ɲ", "j", "i" },
			["o"] = { "ɲ", "ɔ" },
			["ó"] = { "ɲ", "u" },
			["u"] = { "ɲ", "u" },
			[false] = { "ɲ", "i" }
		},
		[false] = "n"
	},
	["ń"] = "ɲ",
	["o"] = {
		["o"] = { "ɔ", "ʔ", "ɔ" }, -- żaroodporny, ognioodporny, etc.
		[false] = "ɔ",
	},
	["ó"] = "u",
	["p"] = {
		["i"] = {
			-- piątek, piasek, etc.
			["a"] = { "pʲ", "j", "a" },
			["ą"] = { "pʲ", "j", "ɔ̃" },
			["e"] = { "pʲ", "j", "ɛ" },
			["ę"] = { "pʲ", "j", "ɛ̃" },
			["i"] = { "pʲ", "j", "i" },
			["o"] = { "pʲ", "j", "ɔ" },
			["ó"] = { "pʲ", "j", "u" },
			["u"] = { "pʲ", "j", "u" },
			[false] = { "pʲ", "i" }
		},
		[false] = "p"
	},
	["r"] = {
		["i"] = {
			["a"] = { "rʲ", "j", "a" },
			["ą"] = { "rʲ", "j", "ɔ̃" },
			["e"] = { "rʲ", "j", "ɛ" },
			["ę"] = { "rʲ", "j", "ɛ̃" },
			["i"] = { "rʲ", "j", "i" },
			["o"] = { "rʲ", "j", "ɔ" },
			["ó"] = { "rʲ", "j", "u" },
			["u"] = { "rʲ", "j", "u" },
			[false] = { "rʲ", "i" }
		},
		["z"] = "ʐ",
		[false] = "r"
	},
	["q"] = {
		["u"] = { "k", "v" },
		[false] = false
	},
	["s"] = {
		["i"] = {
			["ą"] = { "ɕ", "ɔ̃" },
			["a"] = { "ɕ", "a" },
			["e"] = { "ɕ", "ɛ" },
			["ę"] = { "ɕ", "ɛ̃" },
			["o"] = { "ɕ", "ɔ" },
			["ó"] = { "ɕ", "u" },
			["u"] = { "ɕ", "u" },
			["y"] = { "ɕ", "ɨ" },
			[false] = { "ɕ", "i" }
		},
		["z"] = "ʂ",
		[false] = "s",
	},
	["ś"] = "ɕ",
	["t"] = "t",
	["u"] = "u",
	["v"] = {
		["i"] = {
			["a"] = { "vʲ", "j", "a" },
			["ą"] = { "vʲ", "j", "ɔ̃" },
			["e"] = { "vʲ", "j", "ɛ" },
			["ę"] = { "vʲ", "j", "ɛ̃" },
			["i"] = { "vʲ", "j", "i" },
			["o"] = { "vʲ", "j", "ɔ" },
			["ó"] = { "vʲ", "j", "u" },
			["u"] = { "vʲ", "j", "u" },
			[false] = { "vʲ", "i" }
		},
		[false] = "v"
	},
	["w"] = {
		["i"] = {
			["a"] = { "vʲ", "j", "a" },
			["ą"] = { "vʲ", "j", "ɔ̃" },
			["e"] = { "vʲ", "j", "ɛ" },
			["ę"] = { "vʲ", "j", "ɛ̃" },
			["i"] = { "vʲ", "j", "i" },
			["o"] = { "vʲ", "j", "ɔ" },
			["ó"] = { "vʲ", "j", "u" },
			["u"] = { "vʲ", "j", "u" },
			[false] = { "vʲ", "i" }
		},
		["j"] = { "vʲ", "j" }, -- e.g. wjazd,
		[false] = "v"
	},
	["x"] = { "k", "s" },
	["y"] = "ɨ",
	["z"] = {
		["i"] = {
			["ą"] = { "ʑ", "ɔ̃" },
			["a"] = { "ʑ", "a" },
			["e"] = { "ʑ", "ɛ" },
			["ę"] = { "ʑ", "ɛ̃" },
			["o"] = { "ʑ", "ɔ" },
			["ó"] = { "ʑ", "u" },
			["u"] = { "ʑ", "u" },
			[false] = { "ʑ", "i" }
		},
		[false] = "z"
	},
	["ź"] = "ʑ",
	["ż"] = "ʐ",
	["-"] = {},
}

local valid_phone = {
	["a"] = true, ["b"] = true, ["bʲ"] = true, ["d"] = true, ["d͡z"] = true, ["d͡ʑ"] = true,
	["d͡ʐ"] = true, ["ɛ"] = true, ["ɛ̃"] = true, ["f"] = true, ["fʲ"] = true, ["ɡ"] = true,
	["ɡʲ"] = true, ["i"] = true, ["ɨ"] = true, ["j"] = true, ["k"] = true, ["kʲ"] = true,
	["l"] = true, ["lʲ"] = true, ["m"] = true, ["mʲ"] = true, ["n"] = true, ["ŋ"] = true,
	["ɲ"] = true, ["ɔ"] = true, ["ɔ̃"] = true, ["p"] = true, ["pʲ"] = true, ["r"] = true, ["rʲ"] = true,
	["s"] = true, ["ɕ"] = true, ["ʂ"] = true, ["t"] = true, ["t͡s"] = true, ["t͡ɕ"] = true, ["t͡ʂ"] = true,
	["u"] = true, ["v"] = true, ["vʲ"] = true, ["w"] = true, ["x"] = true, ["xʲ"] = true, ["z"] = true,
	["ʑ"] = true, ["ʐ"] = true, ["ʔ"] = true, ["ɣ"] = true
}

local sylmarks = {
	["."] = ".", ["'"] = "ˈ", [","] = "ˌ"
}

local vowel = {
	["a"] = true, ["ɛ"] = true, ["ɛ̃"] = true,
	["i"] = true, ["ɨ"] = true, ["ɔ"] = true,
	["ɔ̃"] = true, ["u"] = true
}

local devoice = {
	["b"] = "p", ["d"] = "t", ["d͡z"] = "t͡s", ["d͡ʑ"] = "t͡ɕ",
	["d͡ʐ"] = "t͡ʂ", ["ɡ"] = "k", ["v"] = "f", ["vʲ"] = "fʲ",
	["z"] = "s", ["ʑ"] = "ɕ", ["ʐ"] = "ʂ",

	-- non-devoicable
	["bʲ"] = "bʲ", ["ɡʲ"] = "ɡʲ", ["m"] = "m", ["mʲ"] = "mʲ",
	["n"] = "n", ["ɲ"] = "ɲ", ["ŋ"] = "ŋ", ["w"] = "w",
	["l"] = "l", ["lʲ"] = "lʲ", ["j"] = "j", ["r"] = "r", ["rʲ"] = "rʲ",
}

local voice = {
	["p"] = "b", ["t"] = "d", ["t͡s"] = "d͡z", ["t͡ɕ"] = "d͡ʑ",
	["t͡ʂ"] = "d͡ʐ", ["k"] = "ɡ", ["f"] = "v", ["fʲ"] = "vʲ",
	["s"] = "z", ["ɕ"] = "ʑ", ["ʂ"] = "ʐ", ["x"] = "ɣ",

	-- non-voicable
	["bʲ"] = "bʲ", ["ɡʲ"] = "ɡʲ", ["m"] = "m", ["mʲ"] = "mʲ",
	["n"] = "n", ["ɲ"] = "ɲ", ["ŋ"] = "ŋ", ["w"] = "w",
	["l"] = "l", ["lʲ"] = "lʲ", ["j"] = "j", ["r"] = "r", ["rʲ"] = "rʲ",
}

local forward_assimilants = {
	["v"] = true, ["vʲ"] = true
}

local denasalized = {
	["ɛ̃"] = "ɛ",
	["ɔ̃"] = "ɔ",
}

local nasal_map = {
	["p"] = "m", ["pʲ"] = "m", ["b"] = "m", ["bʲ"] = "m", -- zębu, klępa
	["k"] = "ŋ", ["kʲ"] = "ŋ", ["ɡ"] = "ŋ", ["ɡʲ"] = "ŋ", ["x"] = "ŋ", -- pąk, łęgowy
	["t"] = "n", ["d"] = "n", -- wątek, piątek, mądrość

	["t͡ɕ"] = "ɲ", ["d͡ʑ"] = "ɲ", ["ɕ"] = "ɲ", ["ʑ"] = "ɲ", -- pięć, pędziwiatr, łabędź
	-- gęsi, więzi
	["t͡ʂ"] = "n", ["d͡ʐ"] = "n", -- pączek, ?
	-- węszyć, mężny
	["t͡s"] = "n", ["d͡z"] = "n", -- wiedząc, pieniędzy
}

local SPECIAL_FLAGS = {
	IS_RZ = "IS_RZ",
}

---
-- Check whether phone doesn't change due to voicing/devoicing
---@param phone string
---@return boolean
local function is_neutral(phone)
	return (devoice[phone] and voice[phone]) and (voice[phone] == devoice[phone])
end

---
-- Check whether phone is a special character (syllable mark or word boundary)
---@param phone string
---@return boolean
local function is_special(phone)
	return phone == " " or sylmarks[phone]
end

---
-- Check whether phone is voiced
---@param phone string
---@return boolean
local function is_voiced(phone)
	return devoice[phone] and phone ~= devoice[phone]
end

---
-- Check whether phone is prone to forward assimilation
---@param phone string
---@param flags table Special flags for this phone
---@return boolean
local function is_forward_assimilant(phone, flags)
	return forward_assimilants[phone] or (flags and flags[SPECIAL_FLAGS.IS_RZ])
end

---
-- Check whether phone cluster is a palatalized cluster
---@param cluster string
---@return boolean
local function is_palatalized_cluster(cluster)
	return cluster:find("[ɡxkfbmprv]ʲj[aɔ̃ɛɛ̃iɔu]") ~= nil
end

---
-- Process special flags for grapheme and associate them with the recorded phone
---@param grapheme string
---@return table | nil
local function process_special_flags(grapheme)
	if grapheme == "rz" then
		return { [SPECIAL_FLAGS.IS_RZ] = true }
	end
end

---
-- Convert letters and graphemes to phones
---@param word string
---@return table<number, string>, table<number, table<string, boolean>>
local function convert_to_phones(word)
	local phones = {}
	local flags = {}
	local chbuf = ""
	local function append_phone(phone)
		table.insert(phones, phone)

		-- mark rz for assimilation later
		local grapheme_flags = process_special_flags(chbuf)
		if grapheme_flags then
			flags[#phones] = grapheme_flags
		end
		chbuf = ""
	end

	local l2ptab = letters2phones
	for ch in mw.ustring.gmatch(mw.ustring.lower(word), ".") do
		local value = l2ptab[ch]

		if value == nil then
			value = l2ptab[false]
			if value == false then
				return nil
			elseif type(value) == "table" then
				for _, phone in ipairs(value) do
					append_phone(phone)
				end
			else
				append_phone(value)
			end
			l2ptab = letters2phones
			value = l2ptab[ch]
		end

		chbuf = chbuf .. ch

		if type(value) == "table" then
			if value[false] == nil then
				for _, phone in ipairs(value) do
					append_phone(phone)
				end
				l2ptab = letters2phones
			else
				l2ptab = value
			end
		elseif type(value) == "string" then
			append_phone(value)
			l2ptab = letters2phones
		else
			append_phone(ch)
		end
	end

	if l2ptab ~= letters2phones then
		local value = l2ptab[false]
		if type(value) == "table" then
			for _, phone in ipairs(value) do
				append_phone(phone)
			end
		else
			append_phone(value)
		end
	end

	return phones, flags
end

---
-- Simplify nasals
---@param phones table<number, string>
---@return table<number, string>
local function simplify_nasals(phones, flags)
	local new_phones, new_flags = {}, {}
	for i, phone in ipairs(phones) do
		local pnext = phones[i + 1]
		if denasalized[phone] then
			if phone == "ɛ̃" and (not pnext or not valid_phone[pnext]) then
				-- denasalize word-final ę
				table.insert(new_phones, denasalized[phone])
				new_flags[#new_phones] = flags[i]
			elseif nasal_map[pnext] then
				table.insert(new_phones, denasalized[phone])
				table.insert(new_phones, nasal_map[pnext])
				new_flags[#new_phones] = flags[i]
			else
				table.insert(new_phones, phone)
				new_flags[#new_phones] = flags[i]
			end
		else
			table.insert(new_phones, phone)
			new_flags[#new_phones] = flags[i]
		end
	end
	return new_phones, new_flags
end

---
-- Devoice consonant phones in terminal positions
---@param phones table<number, string> Target phone table to mutate
local function terminal_devoice(phones)
	local final_phone = phones[#phones]
	if is_voiced(final_phone) then
		phones[#phones] = devoice[final_phone]
	end
end

---
-- Process consonant cluster assimilation for single cluster
---@param cluster table<number, string> Consonant cluster
---@param flags table<number, table<string, boolean>> Flags relative to the cluster
---@param new_phones table<number, string> Target phone table to mutate
local function process_consonant_cluster(cluster, flags, new_phones)
	local determining_index = #cluster
	while cluster[determining_index] do
		local candidate = cluster[determining_index]
		-- Skip forward assimilants and neutral phones to find the first voiced/devoiced consonant which decides the entire cluster
		if not is_forward_assimilant(candidate, flags[determining_index]) and not is_neutral(candidate) and not is_special(candidate) then
			break
		end
		determining_index = determining_index - 1
	end

	-- If the cluster ends up being comprised of just neutral phones and forward assimilants, add it as-is
	if determining_index == 0 then
		for _, consonant in ipairs(cluster) do
			table.insert(new_phones, consonant)
		end
		return
	end

	-- Transform the entire cluster, forward and back, relative to the determining consonant's voicing
	local determining_consonant = cluster[determining_index]
	local target_map = is_voiced(determining_consonant) and voice or devoice

	for _, consonant in ipairs(cluster) do
		local transformed = target_map[consonant] or consonant
		table.insert(new_phones, transformed)
	end
end

---
-- Process consonant cluster assimilation for single cluster
---@param phones table<number, string>
---@param flags table<number, table<string, boolean>>
---@return table<number, string>
local function process_consonant_clusters(phones, flags)
	local new_phones = {}
	local i = 1
	while i <= #phones do
		local pcurr, pnext = phones[i]
		if not valid_phone[pcurr] or vowel[pcurr] then
			-- Other phone encountered, add it as-is
			table.insert(new_phones, pcurr)
		else
			-- Consonant cluster to process
			local cluster = {}
			-- Phone flags indexed relative to the cluster
			local cluster_flags = {}

			-- Search forward for consonant cluster
			local j = i
			while j <= #phones do
				pnext = phones[j]

				-- Break on vowel or invalid symbol and process what we have
				if vowel[pnext] or (not valid_phone[pnext] and not is_special(pnext)) then
					break
				end

				table.insert(cluster, pnext)
				-- Set the cluster-relative flag for the latest processed phoneme
				cluster_flags[#cluster] = flags[j]

				j = j + 1
			end

			if #cluster > 0 then
				if #cluster > 1 then
					-- Process actual consonant cluster
					process_consonant_cluster(cluster, cluster_flags, new_phones)
					-- Skip forward past the processed phones to avoid any unwanted duplication
					-- Offset by 1 to compensate, because i is unconditionally incremented by 1 at the very end
					i = j - 1
				else
					-- The cluster is a single consonant, add it as-is
					table.insert(new_phones, cluster[1])
				end
			end
		end
		i = i + 1
	end
	return new_phones
end

---
-- Join several phones together, handling table and nil values
---@vararg string | table Phones to join together
---@return string
local function join_phones(...)
	local args = {...}
	local str = ""
	for _, syllable in ipairs(args) do
		if type(syllable) == "table" then
			str = str .. table.concat(syllable, "")
		else
			str = str .. (syllable or "")
		end
	end
	return str
end

---
-- Group phones into syllables
---@param phones table<number, string>
---@return table<number, string>
local function collect_syllables(phones)
	local words, curword, sylmarked, sylbuf = {}, nil, false
	for i, pcurr in ipairs(phones) do
		local pprev, pnext, pnnext = phones[i - 1], phones[i + 1], phones[i + 2]

		if valid_phone[pcurr] then
			if not curword then
				curword, sylbuf, had_vowl, sylmarked = {}, '', false, false
				table.insert(words, curword)
			end

			local same_syl = true

			if vowel[pcurr] then
				if had_vowl then
					same_syl = false
				end
				had_vowl = true
			elseif had_vowl then
				if vowel[pnext] then
					same_syl = false
				elseif not vowel[pprev] and not vowel[pnext] then
					same_syl = false
				elseif vowel[pprev] and is_palatalized_cluster(join_phones(pcurr, pnext, pnnext)) then
					same_syl = false
				elseif ((pcurr == "s") and ((pnext == "t") or (pnext == "p") or (pnext == "k")))
						or (pnext == "r") or (pnext == "f") or (pnext == "w")
						or ((pcurr == "ɡ") and (pnext == "ʐ"))
						or ((pcurr == "d") and ((pnext == "l") or (pnext == "w") or (pnext == "ɲ")))
						or is_palatalized_cluster(join_phones(pprev, pcurr, pnext))
				then
					-- these should belong to a common syllable
					same_syl = true
				end
			end

			if same_syl then
				sylbuf = sylbuf .. pcurr
			else
				table.insert(curword, sylbuf)
				sylbuf, had_vowl = pcurr, vowel[pcurr]
			end
		elseif (curword or valid_phone[pnext]) and sylmarks[pcurr] then
			if not curword then
				curword, sylbuf, had_vowl = {}, '', false
				table.insert(words, curword)
			end
			sylmarked = true
			if sylbuf then
				table.insert(curword, sylbuf)
				sylbuf = ''
			end
			table.insert(curword, sylmarks[pcurr])
		else
			if sylbuf then
				if #curword > 0 and not had_vowl then
					curword[#curword] = curword[#curword] .. sylbuf
				else
					table.insert(curword, sylbuf)
				end
				if sylmarked then
					words[#words] = table.concat(curword)
				end
			end
			curword, sylbuf = nil, nil
			table.insert(words, pcurr)
		end
	end
	if sylbuf then
		if #curword > 0 and not had_vowl then
			curword[#curword] = curword[#curword] .. sylbuf
		else
			table.insert(curword, sylbuf)
		end
		if sylmarked then
			words[#words] = table.concat(curword)
		end
	end

	return words
end

function export.convert_to_IPA(word)
	local phones, flags = convert_to_phones(word)
	phones, flags = simplify_nasals(phones, flags)
	terminal_devoice(phones)
	phones = process_consonant_clusters(phones, flags)
	local words = collect_syllables(phones)

	-- mark syllable breaks and stress
	for i, word in ipairs(words) do
		if type(word) == "table" then
			-- unless already marked
			if not ((word[2] == ".") or (word[2] == "ˈ") or (word[2] == "ˌ")) then
				for j, syl in ipairs(word) do
					if j == (#word - 1) then
						word[j] = "ˈ" .. syl
					elseif j ~= 1 then
						word[j] = "." .. syl
					end
				end
			end
			words[i] = table.concat(word)
		end
	end
	
	-- get rid of /ʲ/
	for i, word in ipairs(words) do
		words[i] = mw.ustring.gsub(words[i], "ʲ([ij])", "%1")
		words[i] = mw.ustring.gsub(words[i], "ʲ", "j")
	end

	return table.concat(words)
end

function export.show(frame)
	local page_title = mw.title.getCurrentTitle().text

	local args = require "Module:parameters".process(frame:getParent().args, {
		[1] = { list = true },
		["qual"] = { list = true, allow_holes = true },
		["n"] = { list = true, allow_holes = true },
	})

	local Array = require "Module:array"

	local words
	if next(args[1]) ~= nil then
		words = args[1]
	else
		words = { page_title }
	end

	local transcriptions = Array(words):map(function(word, i)
		local qualifiers = { args.qual[i] }
		return {
			pron = "/" .. export.convert_to_IPA(word) .. "/",
			qualifiers = #qualifiers > 0 and qualifiers or nil,
			note = args.n[i]
		}
	end)

	return m_IPA.format_IPA_full(lang, transcriptions)
end

return export