Modul:ru-translit

Dari Wikikamus bahasa Indonesia, kamus bebas

Dokumentasi untuk modul ini dapat dibuat di Modul:ru-translit/doc

local export = {}

--[=[

FIXME:

1. (DONE) If you write '''Б'''ез, it transliterates to '''B'''jez instead of
   '''B'''ez, as it should.
2. (DONE) Convert ъ to nothing before comma or other non-letter particle, e.g.
   in Однимъ словомъ, идешь на чтеніе.
3. (DONE) Make special-casing for adjectives in -го and for что (and friends)
    be the default, and implement transformations in Cyrillic rather than after
    translit so that we can display the transformed Cyrillic in the
    "phonetic respelling" notation of {{ru-IPA}}.
4. (DONE) Convert apostrophe to ъ before transliteration when after a consonant
   and before a vowel (requested by Atitarev).
]=]

local u = mw.ustring.char
local rfind = require("Module:string utilities").find
local rsub = require("Module:string utilities").gsub
local rmatch = require("Module:string utilities").match
local rsplit = mw.text.split
local ulower = require("Module:string utilities").lower
local usub = require("Module:string utilities").sub
local toNFC = mw.ustring.toNFC
local toNFD = mw.ustring.toNFD
local decompose = require("Module:ru-common").decompose

local AC = u(0x301) -- acute = ́
local GR = u(0x0300) -- grave = ̀
local DI = u(0x0308) -- diaeresis = ̈
local TEMP_G = u(0xFFF1) -- substitute to preserve g from changing to v

local function ine(x) -- if not empty
	if x == "" then return nil else return x end
end

-- In this table, we now map Cyrillic е and э to je and e, and handle the
-- post-consonant version (plain e and ɛ) specially.
local tab = {
	["А"]="A", ["Б"]="B", ["В"]="V", ["Г"]="G", ["Д"]="D", ["Е"]="Je", ["Ж"]="Ž", ["З"]="Z", ["И"]="I", ["Й"]="J",
	["К"]="K", ["Л"]="L", ["М"]="M", ["Н"]="N", ["О"]="O", ["П"]="P", ["Р"]="R", ["С"]="S", ["Т"]="T", ["У"]="U", ["Ф"]="F",
	["Х"]="X", ["Ц"]="C", ["Ч"]="Č", ["Ш"]="Š", ["Щ"]="Šč", ["Ъ"]="ʺ", ["Ы"]="Y", ["Ь"]="ʹ", ["Э"]="E", ["Ю"]="Ju", ["Я"]="Ja",
	['а']='a', ['б']='b', ['в']='v', ['г']='g', ['д']='d', ['е']='je', ['ж']='ž', ['з']='z', ['и']='i', ['й']='j',
	['к']='k', ['л']='l', ['м']='m', ['н']='n', ['о']='o', ['п']='p', ['р']='r', ['с']='s', ['т']='t', ['у']='u', ['ф']='f',
	['х']='x', ['ц']='c', ['ч']='č', ['ш']='š', ['щ']='šč', ['ъ']='ʺ', ['ы']='y', ['ь']='ʹ', ['э']='e', ['ю']='ju', ['я']='ja',
	-- Russian style quotes
	['«']='“', ['»']='”',
	-- archaic, pre-1918 letters
	['І']='I', ['і']='i', ['Ѳ']='F', ['ѳ']='f',
	['Ѣ']='Jě', ['ѣ']='jě', ['Ѵ']='I', ['ѵ']='i',
	-- archaic, pre-1700 letters
	['Ѕ']='Z', ['ѕ']='z', ['Ꙃ']='Z', ['ꙃ']='z', ['Ꙁ']='Z', ['ꙁ']='z',
	['Ѡ']='O', ['ѡ']='o', ['Ѿ']='Ot', ['ѿ']='ot', ['Ꙋ']='U', ['ꙋ']='u',
	['Ꙑ']='Y', ['ꙑ']='y', ['Ꙗ']='Ja', ['ꙗ']='ja', ['Ѥ']='Je', ['ѥ']='je',
	['Ѧ']='Ja', ['ѧ']='ja', ['Ѩ']='Ja', ['ѩ']='ja', ['Ѫ']='U', ['ѫ']='u', ['Ѭ']='Ju', ['ѭ']='ju',
	['Ѯ']='Ks', ['ѯ']='ks', ['Ѱ']='Ps', ['ѱ']='ps', ['Є']='E', ['є']='e', ['Ї']='I', ['ї']='i',
}

-- following based on ru-common for use with is_monosyllabic()
-- any Cyrillic or Latin vowel, including ёЁ and composed Cyrillic vowels with grave accent
local lowercase_vowels = "аеѐёєиѝіїоуүꙋѡѿꙑыѣэюꙗяѥѧѫѩѭѵaáàeéèěɛiíìoóòǒǫuúùyýỳ"
local uppercase_vowels = "АЕЀЁЄИЍІЇОУҮꙊѠѾꙐЫѢЭЮꙖЯѤѦѪѨѬѴAÁÀEÉÈĚƐIÍÌOÓÒǑǪUÚÙYÝỲ"
local vowels = lowercase_vowels .. uppercase_vowels

-- FIXME! Doesn't work with ɣ, which gets included in this character set
local non_consonants = "[" .. vowels .. "ЪЬъьʹʺ%A]"
local consonants = "[^" .. vowels .. "ЪЬъьʹʺ%A]"

local map_to_plain_e_map = {["Е"] = "E", ["е"] = "e", ["Ѣ"] = "Ě", ["ѣ"] = "ě", ["Э"] = "Ɛ", ["э"] = "ɛ"}
local function map_to_plain_e(pre, e)
	return pre .. map_to_plain_e_map[e]
end

local map_to_je_map = {["Е"] = "Je", ["е"] = "je", ["Ѣ"] = "Jě", ["ѣ"] = "jě", ["Э"] = "E", ["э"] = "e"}
local function map_to_je(pre, e)
	if e == nil then
		e = pre
		pre = ""
	end
	return pre .. map_to_je_map[e]
end

-- True if Cyrillic or decomposed Latin word has no more than one vowel;
-- includes non-syllabic stems such as льд-; copied from ru-common and modified
-- to avoid having to import that module (which would slow things down
-- significantly)
local function is_monosyllabic(word)
	return not rfind(word, "[" .. vowels .. "].*[" .. vowels .. "]")
end

-- Apply transformations to the Cyrillic to more closely match pronunciation.
-- Return two arguments: the "original" text (after decomposing composed
-- grave characters), and the transformed text. If the two are different,
-- {{ru-IPA}} should display a "phonetic respelling" notation. 
-- NOADJ disables special-casing for adjectives in -го, while FORCEADJ forces
-- special-casing for adjectives, including those in -аго (pre-reform spelling)
-- and disables checking for exceptions (e.g. много, ого). NOSHTO disables
-- special-casing for что and related words.
function export.apply_tr_fixes(text, noadj, noshto, forceadj)
	-- decompose stress accents without decomposing letters we want to treat
	-- as units (e.g. й or ё)
	text = decompose(text)

	local origtext = text
	-- the second half of the if-statement below is an optimization; see above.
	if not noadj and text:find("го") then
		local v = {["г"] = "в", ["Г"] = "В"}
		local repl = function(e, g, o, sja) return e .. v[g] .. o .. (sja or "") end
		-- Handle какого-нибудь/-либо/-то; must be done first because of an exception
		-- made for бого-, снего-, etc.
		text = rsub(text, "([кКтТ][аА][кК][оеОЕ" .. (forceadj and "аА" or "") .. "][\204\129\204\128]?)([гГ])([оО]%-)", repl)
		if not forceadj then
			local function go(text, case)
				local pattern = rsub(case, "^(.)(.*)(го[\204\129\204\128]?)(%-?)$", function(m1, m2, m3, m4)
					m1 = "%f[%a\204\129\204\128]([" .. mw.ustring.upper(m1) .. m1 .. "]"
					m2 = m2:gsub("\204[\128\129]", "[\204\129\204\128]?") .. ")"
					m3 = m3:gsub("\204[\128\129]", "[\204\129\204\128]?")
						:gsub("^г(.*)", "г(%1")
					m4 = m4 == "-" and "%-)" or ")%f[^%a\204\129\204\128]"
					return m1 .. m2 .. m3 .. m4
				end)
				return rsub(text, pattern, "%1" .. TEMP_G .. "%2")
			end
			for _, case in ipairs{"мно́го", "н[еа]мно́го", "до́рого", "недо́рого", "стро́го", "нестро́го", "на́строго", "убо́го", "пол[ао]́го"} do
				text = go(text, case)
			end
			-- check for neuter short forms of compound adjectives in -но́гий
			if rfind(text, "но[\204\129\204\128]?го%f[^%a\204\129\204\128]") then
				for _, case in ipairs{"безно́го", "босоно́го", "веслоно́го", "длинноно́го", "двуно́го", "коротконо́го", "кривоно́го", "одноно́го", "пятино́го", "трёхно́го", "трехно́го", "хромоно́го", "четвероно́го", "шестино́го"} do
					text = go(text, case)
				end
			end
			for _, case in ipairs{"ого́", "го́го", "ваго́го", "ло́го", "п[ео]́го", "со́го", "То́го", "ле́го", "игого́", "огого́", "альбиньязего", "д[иі]е́го", "бо́лого", "гр[иі]е́го", "манче́го", "пичис[иі]е́го", "тенкодого", "хио́го", "аго-", "его-", "ого-"} do
				text = go(text, case)
			end
		end
		--handle genitive/accusative endings, which are spelled -ого/-его/-аго
		-- (-ogo/-ego/-ago) but transliterated -ovo/-evo/-avo; only for adjectives
		-- and pronouns, excluding words like много, ого (-аго occurs in
		-- pre-reform spelling); \204\129 is an acute accent, \204\128 is a grave accent
		local pattern = "([оеОЕ" .. (forceadj and "аА" or "") .. "][\204\129\204\128]?)([гГ])([оО][\204\129\204\128]?)"
		local reflexive = "([сС][яЯ][\204\129\204\128]?)"
		text = rsub(text, pattern .. "%f[^%a\204\129\204\128" .. TEMP_G .. "]", repl)
		text = rsub(text, pattern .. reflexive .. "%f[^%a\204\129\204\128" .. TEMP_G .. "]", repl)
		-- handle сегодня
		text = rsub(text, "%f[%a\204\129\204\128]([Сс]е)г(о[\204\129\204\128]?дня)%f[^%a\204\129\204\128]", "%1в%2")
		-- handle сегодняшн-
		text = rsub(text, "%f[%a\204\129\204\128]([Сс]е)г(о[\204\129\204\128]?дняшн)", "%1в%2")
		-- replace TEMP_G with g; must be done after the -go -> -vo changes
		text = rsub(text, TEMP_G, "г")
	end

	-- the second half of the if-statement below is an optimization; see above.
	if not noshto and text:find("то") then
		local ch2sh = {["ч"] = "ш", ["Ч"] = "Ш"}
		-- Handle что
		text = rsub(text, "%f[%a\204\129\204\128]([Чч])(то[\204\129\204\128]?)%f[^%a\204\129\204\128]",
			function(ch, to) return ch2sh[ch] .. to end)
		-- Handle чтобы, чтоб
		text = rsub(text, "%f[%a\204\129\204\128]([Чч])(то[\204\129\204\128]?бы?)%f[^%a\204\129\204\128]",
			function(ch, to) return ch2sh[ch] .. to end)
		-- Handle ничто
		text = rsub(text, "%f[%a\204\129\204\128]([Нн]и)ч(то[\204\129\204\128]?)%f[^%a\204\129\204\128]", "%1ш%2")
	end

	-- Handle мягкий, лёгкий, легчать, etc.
	text = rsub(text, "([МмЛл][яеё][\204\129\204\128]?)г([кч])", "%1х%2")

	return origtext, text
end

-- Transliterate after the pronunciation-related transformations of
-- export.apply_tr_fixes() have been applied. Called from {{ru-IPA}}.
-- INCLUDE_MONOSYLLABIC_JO_ACCENT is as in export.tr().
function export.tr_after_fixes(text, include_monosyllabic_jo_accent)
	-- Remove word-final hard sign, either utterance-finally or followed by
	-- a non-letter character such as space, comma, period, hyphen, etc.
	text = rsub(text, "[Ъъ]%f[%z%A]", "")
	
	-- Make izhitsa display as -v- if following /a/
	text = rsub(text, "([АаЯяꙖꙗѦѧѨѩ])([Ѵѵ])", function(m1, m2)
		return m1 .. (m2 == "Ѵ" and "В" or "в")
	end)
	
	-- Convert apostrophe the hard sign between consonant and vowel (i.e.
	-- in the places where the hard sign normally occurs in modern text).
	-- Apostrophe is sometimes used to indicate the hard sign; this may have
	-- originated from the forcible removal of the hard sign from printing
	-- offices in the 1920's, after the implementation of the Russian
	-- orthography reform. The if-statement is an optimization; see below.
	if rfind(text, "'") then
		text = rsub(text, "(" .. consonants .. ")'([" .. lowercase_vowels .. "])", "%1ъ%2")
		text = rsub(text, "(" .. consonants .. ")'([" .. uppercase_vowels .. "])", "%1Ъ%2")
	end
	
	-- Convert ё (and archaic friends) here, since they need to be dealt with
	-- as a special-case. In general terms, ё becomes jo, ѣ̈ becomes jǒ and я̈,
	-- ѧ̈ & ѩ̈ become jǫ. When determining stress, they are treated
	-- as interchangeable.
	-- Technical explanation: all ten letters are converted to otherwise-unused
	-- ASCII characters \14 to \19 (merging я̈, ѧ̈ and ѩ̈ as they are converted to
	-- the same thing, which also avoids using \9 to \13 as they are in %s; see
	-- below). Because the rules below are varied (e.g. multiple capture groups,
	-- initial J/j and stress only sometimes added etc), it's not possible to
	-- use a single gsub table for all of them. To keep things fast and to
	-- avoid multiple conversion tables, the characters are converted to higher
	-- temporary codepoints using simple addition to denote that they've been
	-- dealt with, so they don't get picked up by a later rule. At the end,
	-- they're then converted into their final forms. Since the o/ǒ/ǫ in the
	-- output is always lowercase (as the j is only ever omitted when the
	-- letter is not initial), the upper and lowercase versions can be merged
	-- at this stage (e.g. Ё & ё use \20, Ѣ̈ & ѣ̈ use \21 etc).
	text = toNFD(text)
	if text:find(DI) then
		local mono = is_monosyllabic(text)
		text = text:gsub("[\208\209][\143\149\162\163\166-\169\175\181]" .. DI, {["Е" .. DI] = "\14", ["е" .. DI] = "\15", ["Ѣ" .. DI] = "\16", ["ѣ" .. DI] = "\17", ["Я" .. DI] = "\18", ["я" .. DI] = "\19", ["Ѧ" .. DI] = "\18", ["ѧ" .. DI] = "\19", ["Ѩ" .. DI] = "\18", ["ѩ" .. DI] = "\19"})
		-- in a nonmonosyllabic word where there is a later stressed vowel
		-- in the same word (e.g. трёхэта́жный), don't put an accent mark on ё.
		text = rsub(text, "([\14-\19])([^ %-]-[" .. vowels .. "]́)", function(m1, m2)
			local n = m1:byte()
			local cap = n % 2 == 0 and true
			return (cap and "J" or "j") .. string.char(math.floor(n / 2) + 13) .. m2
		end)
		-- same goes if there are two ё's in a word (трёхколёсный, четырёхзвёздный)
		text = rsub(text, "([\14-\19])([^ %-]-[\14-\19])", function(m1, m2)
			local n = m1:byte()
			local cap = n % 2 == 0 and true
			return (cap and "J" or "j") .. string.char(math.floor(n / 2) + 13) .. m2
		end)
		-- same goes if this is a prefix (четырёх-)
		text = rsub(text, "([\14-\19])([^ %-]-%-)$", function(m1, m2)
			local n = m1:byte()
			local cap = n % 2 == 0 and true
			return (cap and "J" or "j") .. string.char(math.floor(n / 2) + 13) .. m2
		end)
		-- same goes if there is a stressed vowel *followed* by ё (this is
		-- quite rare but occurs e.g. in А́ндзё "Anjō (city in Japan)"
		text = rsub(text, "([" .. vowels .. "]́[^ %-]-)([\14-\19])", function(m1, m2)
			local n = m2:byte()
			local cap = n % 2 == 0 and true
			return m1 .. (cap and "J" or "j") .. string.char(math.floor(n / 2) + 13)
		end)
		-- main conversion of ё
		text = text:gsub("[\14-\19]", function(m)
			local n = m:byte()
			local cap = n % 2 == 0 and true
			local ret = (cap and "J" or "j") .. string.char(math.floor(n / 2) + 13)
			-- monosyllabic ё has no accent when INCLUDE_MONOSYLLABIC_JO_ACCENT
			-- isn't set
			if include_monosyllabic_jo_accent or (not mono) or text:find("^%-") then
				ret = ret .. AC
			end
			return ret
		end)
		-- handle removal of j after a hushing consonant (on the assumption
		-- that a medial capital J is in an initialism, so should not be
		-- removed)
		text = rsub(text, "([ЖжЧчШшЩщ])j", "%1")
		-- convert the temporary characters to their final forms
		local reconvert = {"o", "ǒ", "ǫ"}
		text = text:gsub("[\20-\22]", function(m)
			return reconvert[m:byte() - 19]
		end)
	end
	
	text = decompose(text)
	
	-- ю after ж and ш becomes u (e.g. брошюра, жюри)
	text = rsub(text, "([жшЖШ])ю","%1u")

	-- the if-statement below isn't necessary but may speed things up in that
	-- in the majority of cases where the letters below don't occur, we avoid
	-- six pattern subs.
	if rfind(text, "[ЕеѢѣЭэ]") then
		-- е after a dash at the beginning of a word becomes e, and э becomes ɛ
		-- (like after a consonant)
		text = rsub(text, "^(%-)([ЕеѢѣЭэ])", map_to_plain_e)
		text = rsub(text, "(%s%-)([ЕеѢѣЭэ])", map_to_plain_e)
		-- don't get confused by single quote or parens between consonant and е;
		-- e.g. Б'''ез''', американ(ец)
		text = rsub(text, "(" .. consonants .. "['%(%)]*)([ЕеѢѣЭэ])", map_to_plain_e)

		-- This is now the default
		-- е after a vowel or at the beginning of a word becomes je, and э becomes e
		-- text = rsub(text, "^([ЕеѢѣЭэ])", map_to_je)
		-- text = rsub(text, "(" .. non_consonants .. ")([ЕеѢѣЭэ])", map_to_je)
		-- -- need to do it twice in case of sequences of such vowels
		-- text = rsub(text, "^([ЕеѢѣЭэ])", map_to_je)
		-- text = rsub(text, "(" .. non_consonants .. ")([ЕеѢѣЭэ])", map_to_je)
	end
	
	-- Main substitution. Also remove any duplicated accents: they shouldn't really happen, but are sometimes possible if ё́ or ё̀ are used.
	text = toNFD(rsub(text,'.', tab))
		:gsub("\204[\128\129](\204[\128\129])", "%1")
	return toNFC(text)
end

-- Transliterates text, which should be a single word or phrase. It should
-- include stress marks, which are then preserved in the transliteration.
-- ё is a special case: it is rendered (j)ó in multisyllabic words and
-- monosyllabic words in multi-word phrases, but rendered (j)o without an
-- accent in isolated monosyllabic words, unless INCLUDE_MONOSYLLABIC_JO_ACCENT
-- is specified. (This is used in conjugation and declension tables.)
-- NOADJ disables special-casing for adjectives in -го, while FORCEADJ forces
-- special-casing for adjectives and disables checking for exceptions
-- (e.g. много). NOSHTO disables special-casing for что and related words.
function export.tr(text, lang, sc, include_monosyllabic_jo_accent, noadj, noshto, forceadj)
	local origtext, subbed_text = export.apply_tr_fixes(text, noadj, noshto, forceadj)
	return export.tr_after_fixes(subbed_text, include_monosyllabic_jo_accent)
end

-- translit with various special-case substitutions; NOADJ disables
-- special-casing for adjectives in -го, while FORCEADJ forces special-casing
-- for adjectives and disables checking for expections (e.g. много).
-- NOSHTO disables special-casing for что and related words. SUB is used
-- to implement arbitrary substitutions in the Cyrillic text before other
-- transformations are applied and before translit. It is of the form
-- FROM/TO,FROM/TO,...
function export.tr_sub(text, include_monosyllabic_jo_accent, noadj, noshto, sub,
	forceadj)
	if type(text) == 'table' then -- called directly from a template
		include_monosyllabic_jo_accent = ine(text.args.include_monosyllabic_jo_accent)
		noadj = ine(text.args.noadj)
		noshto = ine(text.args.noshto)
		sub = ine(text.args.sub)
		text = text.args[1]
	end

	if sub then
		local subs = rsplit(sub, ",")
		for _, subpair in ipairs(subs) do
			local subsplit = rsplit(subpair, "/")
			text = rsub(text, subsplit[1], subsplit[2])
		end
	end

	return export.tr(text, nil, nil, include_monosyllabic_jo_accent, noadj, noshto, forceadj)
end

--for adjectives, pronouns
function export.tr_adj(text, include_monosyllabic_jo_accent)
	if type(text) == 'table' then -- called directly from a template
		include_monosyllabic_jo_accent = ine(text.args.include_monosyllabic_jo_accent)
		text = text.args[1]
	end

	-- we have to include "forceadj" because typically when tr_adj() is called
	-- from the noun or adjective modules, it's called with suffix ого, which
	-- would otherwise trigger the exceptional case and be transliterated as ogo
	return export.tr(text, nil, nil, include_monosyllabic_jo_accent, false,
		"noshto", "forceadj")
end

return export

-- For Vim, so we get 4-space tabs
-- vim: set ts=4 sw=4 noet: