This module will transliterate text in one of the Northeast Caucasian languages. It is also used to transliterate Aghul, Akhvakh, Andi, Archi, Avar, Budukh, Botlikh, Chechen, Chamalal, Ingush, Bezhta, and Bagvalal. The module should preferably not be called directly from templates or other modules. To use it from a template, use {{xlit}}. Within a module, use Module:languages#Language:transliterate.

For testcases, see Module:cau-nec-translit/testcases.

Functions

tr(text, lang, sc)
Transliterates a given piece of text written in the script specified by the code sc, and language specified by the code lang.
When the transliteration fails, returns nil.

local m_str_utils = require("Module:string utilities")

local gsub = m_str_utils.gsub
local lower = m_str_utils.lower
local toNFC = mw.ustring.toNFC
local toNFD = mw.ustring.toNFD
local u = m_str_utils.char
local upper = m_str_utils.upper

local CyrlConsonant = "бвгджзклмнпрстфхцчшщБВГДЖЗКЛМНПРСТФХЦЧШЩ"
local CyrlVowel = "аеёиоуыэюяАЕЁИОУЫЭЮЯ"
local ACUTE, CIRC, TILDE, MACRON, BREVE, DOTABOVE, DIAER, CARON, DOTBELOW = u(0x301), u(0x302), u(0x303), u(0x304), u(0x306), u(0x307), u(0x308), u(0x30C), u(0x323)
local accent = "[" .. ACUTE .. CIRC .. TILDE .. MACRON .. BREVE .. DOTABOVE .. DIAER .. CARON .. DOTBELOW .. "]"
local br = u(0xF000)

local export = {}

-- Structured like this to reduce size of loaded table.
local function getSubs(lang)
	--Aghul
	if lang == "agx" then
		return {
			{
				["гъ"] = "ğ", ["гь"] = "h", ["гӏ"] = "ʻʳ", ["къ"] = "qq", ["кь"] = "qʼ", ["кӏ"] = "kʼ", ["пӏ"] = "pʼ", ["тӏ"] = "tʼ", ["хъ"] = "q", ["хь"] = "x̂", ["хӏ"] = "ḥʳ", ["цӏ"] = "cʼ", ["чӏ"] = "čʼ"
			},
			{
				["а"] = "a", ["б"] = "b", ["в"] = "v", ["г"] = "g", ["д"] = "d", ["е"] = "e", ["ё"] = "jo", ["ж"] = "ž", ["з"] = "z", ["и"] = "ı̇", ["й"] = "j", ["к"] = "k", ["л"] = "l", ["м"] = "m", ["н"] = "n", ["о"] = "o", ["п"] = "p", ["р"] = "r", ["с"] = "s", ["т"] = "t", ["у"] = "u", ["ф"] = "f", ["х"] = "x", ["ц"] = "c", ["ч"] = "č", ["ш"] = "š", ["щ"] = "šš", ["ъ"] = "ʔ", ["ы"] = "ə", ["ь"] = "ʲ", ["э"] = "e", ["ю"] = "ju", ["я"] = "ja"
			}
		}
	-- Akhvakh
	elseif lang == "akv" then
		return {
			{
				["гъӏ"] = "ğʰ", ["къӏ"] = "qˣʼ", ["кьӏ"] = "kˡʼ", ["лӏъ"] = "ᵏl", ["хъӏ"] = "qˣ"
			},
			{
				["гъ"] = "ɣ", ["гь"] = "h", ["гӏ"] = "ʻʳ", ["къ"] = "qxʼ", ["кь"] = "kkˡʼ", ["кӏ"] = "kʼ", ["лъ"] = "lˢ", ["ль"] = "ĺ", ["лӏ"] = "ᵏll", ["пӏ"] = "pʼ", ["тӏ"] = "tʼ", ["хъ"] = "qx", ["хь"] = "x̂", ["хӏ"] = "ḥʳ", ["цӏ"] = "cʼ", ["чӏ"] = "čʼ"
			},
			{
				["а"] = "a", ["б"] = "b", ["в"] = "v", ["г"] = "g", ["д"] = "d", ["е"] = "e", ["ё"] = "jo", ["ж"] = "ž", ["з"] = "z", ["и"] = "ı̇", ["й"] = "j", ["к"] = "k", ["л"] = "l", ["м"] = "m", ["н"] = "n", ["о"] = "o", ["п"] = "p", ["р"] = "r", ["с"] = "s", ["т"] = "t", ["у"] = "u", ["ф"] = "f", ["х"] = "x", ["ц"] = "c", ["ч"] = "č", ["ш"] = "š", ["щ"] = "šš", ["ъ"] = "ʔ", ["ы"] = "ə", ["ь"] = "ʲ", ["э"] = "e", ["ю"] = "ju", ["я"] = "ja"
			}
		}
	-- Andi
	elseif lang == "ani" then
		return {
			{
				["къкъ"] = "qxʼ", ["хъхъ"] = "qx"
			},
			{
				["гъӏ"] = "ğʼ", ["жъӏ"] = "žʼ", ["къӏ"] = "qxʼ", ["лъӏ"] = "llˢʼ", ["хъӏ"] = "qx", ["цъӏ"] = "ccʼ", ["чъӏ"] = "cčʼ"
			},
			{
				["гъ"] = "ğ", ["гь"] = "h", ["гӏ"] = "gʼ", ["къ"] = "qˣʼ", ["кь"] = "kkˡʼ", ["кӏ"] = "kʼ", ["лъ"] = "lˢ", ["ль"] = "lˢʼ", ["лӏ"] = "ᵏll", ["пӏ"] = "pʼ", ["тӏ"] = "tʼ", ["хъ"] = "qˣ", ["хь"] = "x̂", ["хӏ"] = "xʼ", ["цӏ"] = "cʼ", ["чӏ"] = "čʼ"
			},
			{
				["а"] = "a", ["б"] = "b", ["в"] = "v", ["г"] = "g", ["д"] = "d", ["е"] = "e", ["ё"] = "jo", ["ж"] = "ž", ["з"] = "z", ["и"] = "ı̇", ["й"] = "j", ["к"] = "k", ["л"] = "l", ["м"] = "m", ["н"] = "n", ["о"] = "o", ["п"] = "p", ["р"] = "r", ["с"] = "s", ["т"] = "t", ["у"] = "u", ["ф"] = "f", ["х"] = "x", ["ц"] = "c", ["ч"] = "č", ["ш"] = "š", ["щ"] = "šš", ["ъ"] = "ˀ", ["ы"] = "ə", ["ь"] = "ʲ", ["э"] = "e", ["ю"] = "ju", ["я"] = "ja"
			}
		}
	-- Archi
	elseif lang == "aqc" then
		return {
			{
				["ккъӏ"] = "qq̣ʼ"
			},
			{
				["гъӏ"] = "ğ̣", ["ккъ"] = "qqʼ", ["къӏ"] = "q̣ʼ", ["хъӏ"] = "q̣", ["хьӏ"] = "x̣"
			},
			{
				["гъ"] = "ğ", ["гь"] = "h", ["гӏ"] = "ˀ", ["къ"] = "qʼ", ["кь"] = "kˡʼ", ["кӏ"] = "kʼ", ["лъ"] = "lʰ", ["ль"] = "lˠ", ["лӏ"] = "ᵏl", ["пӏ"] = "pʼ", ["тӏ"] = "tʼ", ["хъ"] = "q", ["хӏ"] = "ḥʳ", ["цӏ"] = "cʼ", ["чӏ"] = "čʼ"
			},
			{
				["а"] = "a", ["б"] = "b", ["в"] = "w", ["г"] = "g", ["д"] = "d", ["е"] = "e", ["ё"] = "jo", ["ж"] = "ž", ["з"] = "z", ["и"] = "ı̇", ["й"] = "j", ["к"] = "k", ["л"] = "l", ["м"] = "m", ["н"] = "n", ["о"] = "o", ["п"] = "p", ["р"] = "r", ["с"] = "s", ["т"] = "t", ["у"] = "u", ["ф"] = "f", ["х"] = "x", ["ц"] = "c", ["ч"] = "č", ["ш"] = "š", ["щ"] = "šš", ["ъ"] = "ʔ", ["ы"] = "ə", ["ь"] = "", ["э"] = "e", ["ю"] = "ju", ["я"] = "ja"
			}
		}
	-- Avar
	elseif lang == "av" then
		return {
			{
				["гъ"] = "ğ", ["гь"] = "h", ["гӏ"] = "ʻ", ["къ"] = "qxʼ", ["кь"] = "kkˡʼ", ["кӏ"] = "kʼ", ["лъ"] = "lˢ", ["лӏ"] = "ᵏll", ["тӏ"] = "tʼ", ["хъ"] = "qx", ["хь"] = "x̂", ["хӏ"] = "ḥʳ", ["цӏ"] = "cʼ", ["чӏ"] = "čʼ"
			},
			{
				["а"] = "a", ["б"] = "b", ["в"] = "w", ["г"] = "g", ["д"] = "d", ["е"] = "e", ["ё"] = "jo", ["ж"] = "ž", ["з"] = "z", ["и"] = "ı̇", ["й"] = "j", ["к"] = "k", ["л"] = "l", ["м"] = "m", ["н"] = "n", ["о"] = "o", ["п"] = "p", ["р"] = "r", ["с"] = "s", ["т"] = "t", ["у"] = "u", ["ф"] = "f", ["х"] = "x", ["ц"] = "c", ["ч"] = "č", ["ш"] = "š", ["щ"] = "šš", ["ъ"] = "ʔ", ["ы"] = "ə", ["ь"] = "ʲ", ["э"] = "e", ["ю"] = "ju", ["я"] = "ja"
			}
		}
	-- Bagvalal
	elseif lang == "kva" then
		return {
			{
				["гъ"] = "ğ", ["гь"] = "h", ["гӏ"] = "ˀ", ["къ"] = "qʼ", ["кь"] = "kkˡʼ", ["кӏ"] = "kʼ", ["лъ"] = "lˢ", ["лӏ"] = "ᵏll", ["сӏ"] = "sʼ", ["тӏ"] = "tʼ", ["хъ"] = "qx", ["хь"] = "x̂", ["хӏ"] = "ḥ", ["цӏ"] = "cʼ", ["чӏ"] = "čʼ", ["шӏ"] = "šʼ"
			},
			{
				["а"] = "a", ["б"] = "b", ["в"] = "v", ["г"] = "g", ["д"] = "d", ["е"] = "e", ["ё"] = "jo", ["ж"] = "ž", ["з"] = "z", ["и"] = "ı̇", ["й"] = "j", ["к"] = "k", ["л"] = "l", ["м"] = "m", ["н"] = "n", ["о"] = "o", ["п"] = "p", ["р"] = "r", ["с"] = "s", ["т"] = "t", ["у"] = "u", ["ф"] = "f", ["х"] = "x", ["ц"] = "c", ["ч"] = "č", ["ш"] = "š", ["щ"] = "šš", ["ъ"] = "ʔ", ["ы"] = "ə", ["ь"] = "ʲ", ["э"] = "e", ["ю"] = "ju", ["я"] = "ja"
			}
		}
	-- Bezhta
	elseif lang == "kap" then
		return {
			{
				["гъ"] = "ğ", ["гь"] = "h", ["гӏ"] = "ʻ", ["къ"] = "qxʼ", ["кь"] = "kˡʼ", ["кӏ"] = "kʼ", ["лъ"] = "lˢ", ["лӏ"] = "ᵏll", ["пӏ"] = "pʼ", ["тӏ"] = "tʼ", ["хъ"] = "qx", ["хь"] = "x̂", ["хӏ"] = "ḥ", ["цӏ"] = "cʼ", ["чӏ"] = "čʼ"
			},
			{
				["а"] = "a", ["б"] = "b", ["в"] = "v", ["г"] = "g", ["д"] = "d", ["е"] = "e", ["ё"] = "jo", ["ж"] = "ž", ["з"] = "z", ["и"] = "ı̇", ["й"] = "j", ["к"] = "k", ["л"] = "l", ["м"] = "m", ["н"] = "n", ["о"] = "o", ["п"] = "p", ["р"] = "r", ["с"] = "s", ["т"] = "t", ["у"] = "u", ["ф"] = "f", ["х"] = "x", ["ц"] = "c", ["ч"] = "č", ["ш"] = "š", ["щ"] = "šš", ["ъ"] = "ʔ", ["ы"] = "ə", ["ь"] = "ʲ", ["э"] = "e", ["ю"] = "ju", ["я"] = "ja"
			}
		}
	-- Botlikh
	elseif lang == "bph" then
		return {
			{
				["гъ"] = "ğ", ["гь"] = "h", ["къ"] = "qˣʼ", ["кь"] = "kkˡʼ", ["кӏ"] = "kʼ", ["лъ"] = "lˢ", ["лӏ"] = "ᵏll", ["пӏ"] = "pʼ", ["тӏ"] = "tʼ", ["хъ"] = "qˣ", ["хь"] = "x̂", ["цӏ"] = "cʼ", ["чӏ"] = "čʼ"
			},
			{
				["а"] = "a", ["б"] = "b", ["в"] = "w", ["г"] = "g", ["д"] = "d", ["е"] = "e", ["ё"] = "jo", ["ж"] = "ž", ["з"] = "z", ["и"] = "ı̇", ["й"] = "j", ["к"] = "k", ["л"] = "l", ["м"] = "m", ["н"] = "n", ["о"] = "o", ["п"] = "p", ["р"] = "r", ["с"] = "s", ["т"] = "t", ["у"] = "u", ["ф"] = "f", ["х"] = "x", ["ц"] = "c", ["ч"] = "č", ["ш"] = "š", ["щ"] = "šš", ["ъ"] = "ˀ", ["ы"] = "ə", ["ь"] = "ʲ", ["э"] = "e", ["ю"] = "ju", ["я"] = "ja"
			}
		}
	-- Budukh
	elseif lang == "bdk" then
		return {
			{
				["къг"] = "gʰ"
			},
			{
				["гъ"] = "ğ", ["гь"] = "h", ["гӏ"] = "ʻ", ["къ"] = "qq", ["кь"] = "qʼ", ["кӏ"] = "kʼ", ["пӏ"] = "pʼ", ["тӏ"] = "tʼ", ["хъ"] = "q", ["хь"] = "x̂", ["хӏ"] = "ḥ", ["цӏ"] = "cʼ", ["чӏ"] = "čʼ"
			},
			{
				["а"] = "a", ["б"] = "b", ["в"] = "v", ["г"] = "g", ["д"] = "d", ["е"] = "e", ["ё"] = "jo", ["ж"] = "ž", ["з"] = "z", ["и"] = "ı̇", ["й"] = "j", ["к"] = "k", ["л"] = "l", ["м"] = "m", ["н"] = "n", ["о"] = "o", ["п"] = "p", ["р"] = "r", ["с"] = "s", ["т"] = "t", ["у"] = "u", ["ф"] = "f", ["х"] = "x", ["ц"] = "c", ["ч"] = "č", ["ш"] = "š", ["щ"] = "šš", ["ъ"] = "ʔ", ["ы"] = "ı", ["ь"] = "ʲ", ["э"] = "e", ["ю"] = "ju", ["я"] = "ja", ["ӏ"] = "ˀ"
			}
		}
	-- Chamalal
	elseif lang == "cji" then
		return {
			{
				["кӏкӏ"] = "kxʰʼ"
			},
			{
				["гъ"] = "ğ", ["гь"] = "h", ["гӏ"] = "ʻ", ["къ"] = "qxʼ", ["кь"] = "kkˡʼ", ["кӏ"] = "kʼ", ["лъ"] = "lˢ", ["лӏ"] = "ᵏll", ["пӏ"] = "pʼ", ["сӏ"] = "sʼ", ["тӏ"] = "tʼ", ["хъ"] = "qx", ["хь"] = "x̂", ["хӏ"] = "ḥ", ["цӏ"] = "cʼ", ["чӏ"] = "čʼ"
			},
			{
				["а"] = "a", ["б"] = "b", ["в"] = "v", ["г"] = "g", ["д"] = "d", ["е"] = "e", ["ё"] = "jo", ["ж"] = "ž", ["з"] = "z", ["и"] = "ı̇", ["й"] = "j", ["к"] = "k", ["л"] = "l", ["м"] = "m", ["н"] = "n", ["о"] = "o", ["п"] = "p", ["р"] = "r", ["с"] = "s", ["т"] = "t", ["у"] = "u", ["ф"] = "f", ["х"] = "x", ["ц"] = "c", ["ч"] = "č", ["ш"] = "š", ["щ"] = "šš", ["ъ"] = "ʔ", ["ы"] = "ə", ["ь"] = "ʲ", ["э"] = "e", ["ю"] = "ju", ["я"] = "ja"
			}
		}
	-- Chechen and Ingush
	elseif lang == "ce" or lang == "inh" then
		return {
			{
				["ккх"] = "qq", ["рхӏ"] = "rh"
			},
			{
				["гӏ"] = "ğ", ["кх"] = "q", ["къ"] = "qʼ", ["кӏ"] = "kʼ", ["пӏ"] = "pʼ", ["тӏ"] = "tʼ", ["хь"] = "ḥʳ", ["хӏ"] = "h", ["цӏ"] = "cʼ", ["чӏ"] = "čʼ"
			},
			{
				["а"] = "a", ["б"] = "b", ["в"] = "v", ["г"] = "g", ["д"] = "d", ["е"] = "e", ["ё"] = "jo", ["ж"] = "ž", ["з"] = "z", ["и"] = "ı̇", ["й"] = "j", ["к"] = "k", ["л"] = "l", ["м"] = "m", ["н"] = "n", ["о"] = "o", ["п"] = "p", ["р"] = "r", ["с"] = "s", ["т"] = "t", ["у"] = "u", ["ф"] = "f", ["х"] = "x", ["ц"] = "c", ["ч"] = "č", ["ш"] = "š", ["щ"] = "šč", ["ъ"] = "ʔ", ["ы"] = "y", ["ь"] = "ʲ", ["э"] = "e", ["ю"] = "ju", ["я"] = "ja", ["ӏ"] = "ˀ"
			}
		}
	end
end

local function double_with_j(vowel, acute, nasal)
	local ret = vowel .. (nasal ~= "" and TILDE or nasal)
	return ret .. acute .. lower(ret)
end

function export.tr(text, lang, sc)
	local subs = getSubs(lang)
	
	if not subs then
		return nil
	end
	
	-- Convert uppercase palochka to lowercase, along with any "false" palochkas (entered as Latin "I" or "l", Greek "Ι" or Cyrillic "І"). Lowercase palochka is found in tables above.
	text = gsub(text, "[IlΙІӀ]", "ӏ")
	-- Convert dialectal nasal ᵸ written as Latin ᴴ.
	text = gsub(text, "ᴴ", "ᵸ")
	
	-- Decompose precomposed characters, except for ё and й.
	text = gsub(text, "[^ёЁйЙ]", toNFD)
	
	-- Substitute double consonants for macrons over consonants. Add a temporary breaking character after, to prevent the creation of false multigraphs with following characters.
	local function macronToDouble(a, b) return a .. b .. lower(a) .. b .. br end
	text = gsub(text, "([" .. CyrlConsonant .. "])" .. MACRON .. "([ъь])" .. MACRON, macronToDouble)
	text = gsub(text, "([" .. CyrlConsonant .. "ъьЪЬ])" .. MACRON .. "(ӏ)" .. MACRON, macronToDouble)
	text = gsub(text, "([" .. CyrlConsonant .. "])" .. MACRON, macronToDouble)
	
	-- Remove any double hard/soft signs or palochkas this creates.
	text = gsub(text, "([ъьӏЪЬӀ])" .. "([ъьӏ])", function(a, b) if b == lower(a) then return a else return a .. b end end)
	
	-- Contextual substitution of "j" before "е", "w" for "у" and ʷ for "в".
	if lang == "aqc" then
		text = gsub(text, "([" .. CyrlConsonant .. "ъьЪЬ]" .. br .. "?[ӏӀ]?" .. br .. "?)в", "%1ʷ")
	else
		text = gsub(gsub(text, "^е", "jе"), "^Е", "Jе")
		text = gsub(text, "([" .. CyrlVowel .. "%s%p]" .. MACRON .. "?ь?ӏ?ᵸ?)е", "%1jе")
		text = gsub(text, "([%s%p])Е", "%1Jе")
		text = gsub(text, "у([аиоуыэ])", "w%1")
		text = gsub(text, "У([аиоуыэ])", "W%1")
		text = gsub(text, "([" .. CyrlVowel .. "]" .. MACRON .. "?ь?ӏ?ᵸ?)у", "%1w")
		text = gsub(text, "([" .. CyrlConsonant .. "ъьЪЬ]" .. br .. "?)в", "%1ʷ")
	end
	
	-- Add "j" before iotated vowels, and substitute non-iotated equivalents.
	text = gsub(gsub(text, "ё", "jо"), "Ё", "Jо")
	text = gsub(gsub(text, "ю", "jу"), "Ю", "Jу")
	text = gsub(gsub(text, "я", "jа"), "Я", "Jа")
	
	-- Process vowel modifiers.
	text = gsub(text, "([" .. CyrlVowel .. "])(" .. MACRON .. "?)(" .. ACUTE .. "?)(ь?)(" .. MACRON .. "?)(ӏ?)(ᵸ?)", function(vowel, macron1, acute, soft, macron2, palochka, nasal)
		local ret = vowel ..
			(soft ~= "" and DIAER or soft) ..
			(palochka ~= "" and lang == "aqc" and DOTBELOW or "") ..
			(nasal ~= "" and TILDE or nasal)
		if macron1 ~= "" then
			ret = ret .. acute .. lower(ret)
		else
			ret = ret .. acute
		end
		return ret .. (lang ~= "aqc" and palochka or "")
	end)
	
	if lang == "ce" or lang == "inh" then
		text = gsub(text, "([иИ])(" .. ACUTE .. "?" .. ")й(ᵸ?)", double_with_j)
		text = gsub(text, "([уУ]" .. DIAER .. ")(" .. ACUTE .. "?" .. ")й(ᵸ?)", double_with_j)
	end
	
	-- Apply language-specific substitutions by iterating over each subtable. For each one, create a temporary table that stores each substitution in lowercase and uppercase variants. Then, iterate over all substitutions.
	for _,i in ipairs(subs) do
		local t = {}
		-- Create a temporary table, then iterate over all of them.
		for k, v in pairs(i) do
			t[k] = v
			if v == "ʔ" then
				t[gsub(k, "^.", upper)] = gsub(v, "^.", "Ɂ")
			else
				t[gsub(k, "^.", upper)] = gsub(v, "^.", upper)
			end
		end
		for letter, replacement in pairs(t) do
			text = text:gsub(letter, replacement)
		end
	end
	
	-- Reposition apostrophes, remove temporary breaking characters, then decompose.
	text = toNFD(gsub(gsub(gsub(text, "ʼʲ", "ʲʼ"), "ʼʷ", "ʷʼ"), br, ""))
	
	-- When double letters both have a modifier letter and/or an apostrophe, only show on the first or second for readability purposes.
	for letter in string.gmatch("abcdefghijklmnopqrstuvwxyzəɣıʔABCDEFGHIJKLMNOPQRSTUVWXYZƏƔɁʻˀ", ".[\128-\191]*") do
		text = gsub(text, "(ᵏ?)" .. letter .. "(" .. accent .. "?" .. accent .. "?" .. accent .. "?)([ʰʲˡʳˢʷˣˠ]?[ʲʷ]?ʼ?)" .. "%1" .. lower(letter) .. "%2%3", "%1" .. letter .. "%2" .. lower(letter) .. "%2%3")
	end
	
	-- Remove consecutive j/ʲ and w/ʷ.
	text = gsub(gsub(text, "ʲ?([Jj])ʲ?", "%1"), "ʷ?([Ww])ʷ?", "%1")
	
	-- Substitute i for dotted dotless i if not followed by an acute or tilde, then recompose.
	return toNFC(gsub(gsub(text, "ı" .. "(" .. DOTBELOW .. "?)" .. DOTABOVE .. "([^" .. ACUTE .. TILDE .. "])", "i%1%2"), "ı" .. "(" .. DOTBELOW .. "?)" .. DOTABOVE .. "$", "i%1"))
end

return export