Module:Th-rm
| Main | Documentation | Usage |
This module implements {{th-rm}}.
local p = {}
local arguments = require('Module:Arguments')
local tt = require('Module:Tt')
local AUTO_TOOLTIP = 'This romanization was generated automatically. Please add a user-curated romanization as soon as possible.'
local THAI = '[ก-๛]'
-- Initial consonants.
local consonants = {
['ก'] = 'k',
['ข'] = 'kh',
['ฃ'] = 'kh',
['ค'] = 'kh',
['ฅ'] = 'kh',
['ฆ'] = 'kh',
['ง'] = 'ng',
['จ'] = 'ch',
['ฉ'] = 'ch',
['ช'] = 'ch',
['ซ'] = 's',
['ฌ'] = 'ch',
['ญ'] = 'y',
['ฎ'] = 'd',
['ฏ'] = 't',
['ฐ'] = 'th',
['ฑ'] = 'th',
['ฒ'] = 'th',
['ณ'] = 'n',
['ด'] = 'd',
['ต'] = 't',
['ถ'] = 'th',
['ท'] = 'th',
['ธ'] = 'th',
['น'] = 'n',
['บ'] = 'b',
['ป'] = 'p',
['ผ'] = 'ph',
['ฝ'] = 'f',
['พ'] = 'ph',
['ฟ'] = 'f',
['ภ'] = 'ph',
['ม'] = 'm',
['ย'] = 'y',
['ร'] = 'r',
['ล'] = 'l',
['ว'] = 'w',
['ศ'] = 's',
['ษ'] = 's',
['ส'] = 's',
['ห'] = 'h',
['ฬ'] = 'l',
['ฮ'] = 'h',
['อ'] = ''
}
-- Final consonants.
local finals = {
['ก'] = 'k',
['ข'] = 'k',
['ฃ'] = 'k',
['ค'] = 'k',
['ฅ'] = 'k',
['ฆ'] = 'k',
['จ'] = 't',
['ฉ'] = 't',
['ช'] = 't',
['ซ'] = 't',
['ฌ'] = 't',
['ฎ'] = 't',
['ฏ'] = 't',
['ฐ'] = 't',
['ฑ'] = 't',
['ฒ'] = 't',
['ด'] = 't',
['ต'] = 't',
['ถ'] = 't',
['ท'] = 't',
['ธ'] = 't',
['ศ'] = 't',
['ษ'] = 't',
['ส'] = 't',
['บ'] = 'p',
['ป'] = 'p',
['ผ'] = 'p',
['ฝ'] = 'p',
['พ'] = 'p',
['ฟ'] = 'p',
['ภ'] = 'p',
['ง'] = 'ng',
['ญ'] = 'n',
['ณ'] = 'n',
['น'] = 'n',
['ร'] = 'n',
['ล'] = 'n',
['ฬ'] = 'n',
['ม'] = 'm'
}
-- Vowels.
local vowels = {
['เียะ'] = 'ia',
['เีย'] = 'ia',
['เือะ'] = 'uea',
['เือ'] = 'uea',
['เือย'] = 'ueai',
['เียว'] = 'iao',
['ัวะ'] = 'ua',
['ัว'] = 'ua',
['เิ็ย'] = 'oei',
['เิย'] = 'oei',
['เอย'] = 'oei',
['็อย'] = 'oi',
['อย'] = 'oi',
['ุย'] = 'ui',
['วย'] = 'uai',
['ัย'] = 'ai',
['ไย'] = 'ai',
['าย'] = 'ai',
['ใ'] = 'ai',
['ไ'] = 'ai',
['เ็ว'] = 'eo',
['แ็ว'] = 'aeo',
['เา'] = 'ao',
['าว'] = 'ao',
['แว'] = 'aeo',
['เว'] = 'eo',
['ิว'] = 'io',
['ีว'] = 'io',
['ำ'] = 'am',
['ือ'] = 'ue',
['ื'] = 'ue',
['ึ'] = 'ue',
['ิ'] = 'i',
['ี'] = 'i',
['ุ'] = 'u',
['ู'] = 'u',
['เ็'] = 'e',
['เ'] = 'e',
['แ็'] = 'ae',
['แ'] = 'ae',
['โะ'] = 'o',
['โ'] = 'o',
['เาะ'] = 'o',
['อ'] = 'o',
['เอะ'] = 'oe',
['เอ'] = 'oe',
['เะ'] = 'e',
['ะ'] = 'a',
['ั'] = 'a',
['า'] = 'a'
}
-- Longest vowel forms must be checked first.
local vowel_order = {
'เือย',
'เียว',
'เือะ',
'เียะ',
'เีย',
'เือ',
'ัวะ',
'ัว',
'เิ็ย',
'เิย',
'็อย',
'เ็ว',
'แ็ว',
'เอย',
'เา',
'าว',
'เว',
'แว',
'าย',
'ไย',
'ัย',
'ุย',
'วย',
'อย',
'ำ',
'ิว',
'ีว',
'ิ',
'ี',
'ึ',
'ื',
'ุ',
'ู',
'เ็',
'แ็',
'เาะ',
'เอะ',
'เอ',
'เะ',
'ะ',
'ั',
'า',
'เ',
'แ',
'โ',
'อ',
'ใ',
'ไ'
}
local consonant_set = {}
for c in pairs(consonants) do
consonant_set[c] = true
end
-- Thai consonant clusters.
local valid_clusters = {
['กร'] = true,
['กล'] = true,
['กว'] = true,
['ขร'] = true,
['ขล'] = true,
['ขว'] = true,
['คร'] = true,
['คล'] = true,
['คว'] = true,
['ฆร'] = true,
['ฆล'] = true,
['จร'] = true,
['ตร'] = true,
['ปร'] = true,
['ปล'] = true,
['พร'] = true,
['พล'] = true,
['ผล'] = true,
['ทร'] = true,
['ธร'] = true,
['ศร'] = true,
['สร'] = true,
['สล'] = true,
['สว'] = true,
['ซร'] = true,
['ฝร'] = true,
['ฟร'] = true
}
-- ห is silent in these clusters when followed by a vowel.
local h_silent_clusters = {
['หง'] = true,
['หน'] = true,
['หม'] = true,
['หย'] = true,
['หร'] = true,
['หล'] = true,
['หว'] = true,
['หญ'] = true
}
-- ร is silent in these clusters when followed by a vowel.
local r_silent_clusters = {
['จร'] = true,
['ซร'] = true,
['ศร'] = true,
['สร'] = true
}
local function chars(text)
local result = {}
for c in mw.text.gsplit(text, '') do
table.insert(result, c)
end
return result
end
local function isConsonant(c)
return c and consonant_set[c] == true
end
local function isThai(c)
return c and mw.ustring.find(c, THAI) ~= nil
end
local function isFrontVowel(c)
return c == 'เ' or c == 'แ' or c == 'โ' or c == 'ใ' or c == 'ไ'
end
local function skipMarks(c, pos)
while c[pos] and (
c[pos] == '็' or
c[pos] == '่' or
c[pos] == '้' or
c[pos] == '๊' or
c[pos] == '๋' or
c[pos] == 'ํ' or
c[pos] == '๎'
) do
pos = pos + 1
end
return pos
end
local function removeSilent(chars_)
local result = {}
local i = 1
while chars_[i] do
if chars_[i] == '์' then
-- ์ makes the following consonant(s) silent.
i = i + 1
while chars_[i] and (
isConsonant(chars_[i]) or
chars_[i] == '็' or
chars_[i] == '่' or
chars_[i] == '้' or
chars_[i] == '๊' or
chars_[i] == '๋'
) do
i = i + 1
end
else
table.insert(result, chars_[i])
i = i + 1
end
end
return result
end
local function getVowel(c, pos)
local remaining = table.concat(c, '', pos)
for _, form in ipairs(vowel_order) do
local length = mw.ustring.len(form)
if mw.ustring.sub(remaining, 1, length) == form then
return form, vowels[form]
end
end
return nil, nil
end
local function getInitial(c, pos)
local c1 = c[pos]
if not isConsonant(c1) then
return '', pos
end
local c2 = c[pos + 1]
-- ห is not pronounced in หง หน หม หย หร หล หว หญ.
if c1 == 'ห' and c2 and h_silent_clusters[c1 .. c2] then
return consonants[c2], pos + 2, true
end
-- ทร is pronounced s in the relevant Sanskrit-derived words.
-- This is the broad automatic fallback; exceptions can be manually
-- supplied with th_rm.
if c1 == 'ท' and c2 == 'ร' then
return 's', pos + 2, false
end
if c2 and valid_clusters[c1 .. c2] then
-- ร is silent in these clusters.
if r_silent_clusters[c1 .. c2] then
return consonants[c1], pos + 2, false
end
return consonants[c1] .. consonants[c2], pos + 2, false
end
return consonants[c1], pos + 1, false
end
local function getCoda(c, pos)
local current = c[pos]
if not current or not isConsonant(current) then
return '', pos
end
-- These do not act as ordinary codas here.
if current == 'อ' or current == 'ห' or current == 'ฮ' then
return '', pos
end
-- ย and ว are generally part of the vowel/diphthong.
if current == 'ย' or current == 'ว' then
return '', pos
end
if finals[current] then
return finals[current], pos + 1
end
return '', pos + 1
end
local function startsNextSyllable(c, pos)
if not c[pos] or not isConsonant(c[pos]) then
return false
end
if isFrontVowel(c[pos + 1]) then
return true
end
local _, vowel = getVowel(c, pos + 1)
return vowel ~= nil
end
local function parseSyllable(c, start)
local pos = start
if not c[pos] then
return nil, pos
end
local initial = ''
local cluster = false
-- Preposed vowels.
local front = ''
if isFrontVowel(c[pos]) then
front = c[pos]
pos = pos + 1
end
local initialPos = pos
initial, pos, _, cluster =
getInitial(c, pos)
if initial == '' and not isConsonant(c[initialPos]) then
return nil, start + 1
end
local vowel
local vowelForm
if front ~= '' then
-- Handle the vowel forms which contain a preposed vowel.
if front == 'เ' and c[pos] == 'ี' and c[pos + 1] == 'ย' then
vowel = 'ia'
vowelForm = 'เีย'
pos = pos + 2
elseif front == 'เ' and c[pos] == 'ื' and c[pos + 1] == 'อ' then
vowel = 'uea'
vowelForm = 'เือ'
pos = pos + 2
elseif front == 'เ' and c[pos] == 'อ' then
vowel = 'oe'
vowelForm = 'เอ'
pos = pos + 1
elseif front == 'เ' and c[pos] == 'า' then
vowel = 'ao'
vowelForm = 'เา'
pos = pos + 1
elseif front == 'เ' and c[pos] == '็' then
vowel = 'e'
vowelForm = 'เ็'
pos = pos + 1
elseif front == 'แ' and c[pos] == '็' then
vowel = 'ae'
vowelForm = 'แ็'
pos = pos + 1
elseif front == 'โ' and c[pos] == 'ะ' then
vowel = 'o'
vowelForm = 'โะ'
pos = pos + 1
elseif front == 'ใ' or front == 'ไ' then
vowel = 'ai'
vowelForm = front
else
vowel = vowels[front]
vowelForm = front
end
else
vowelForm, vowel = getVowel(c, pos)
if vowelForm then
pos = pos + mw.ustring.len(vowelForm)
end
end
-- Implied vowels.
if not vowel then
if c[pos] and isConsonant(c[pos]) then
if startsNextSyllable(c, pos) then
vowel = 'a'
else
vowel = 'o'
end
else
vowel = 'a'
end
end
pos = skipMarks(c, pos)
-- Final consonant.
local coda = ''
local codaPos = pos
if c[pos] and isConsonant(c[pos]) then
-- If another consonant begins the next syllable, don't consume
-- it as a coda.
if startsNextSyllable(c, pos) then
coda = ''
else
coda, codaPos = getCoda(c, pos)
end
end
pos = codaPos
pos = skipMarks(c, pos)
return {
initial = initial,
vowel = vowel,
coda = coda,
next = pos
}, pos
end
local function shouldHyphen(previous, current)
if not previous or not current then
return false
end
-- Before a succeeding syllable beginning with a vowel.
if current.initial == '' and current.vowel ~= '' then
return true
end
-- Before ng when the preceding syllable ends in a vowel.
if previous.coda == '' and current.initial == 'ng' then
return true
end
return false
end
local function romanizeWord(word)
local c = chars(word)
c = removeSilent(c)
local syllables = {}
local pos = 1
local safety = 0
while pos <= #c and safety < 1000 do
safety = safety + 1
if isConsonant(c[pos]) or isFrontVowel(c[pos]) then
local syllable, nextPos = parseSyllable(c, pos)
if syllable then
table.insert(syllables, syllable)
if nextPos <= pos then
pos = pos + 1
else
pos = nextPos
end
else
-- Fail safely rather than looping forever.
pos = pos + 1
end
else
-- Thai punctuation/marks that aren't part of a syllable.
pos = pos + 1
end
end
local output = {}
for i, syllable in ipairs(syllables) do
if shouldHyphen(syllables[i - 1], syllable) then
table.insert(output, '-')
end
table.insert(
output,
(syllable.initial or '') ..
(syllable.vowel or '') ..
(syllable.coda or '')
)
end
return table.concat(output)
end
-- Parse the text while treating <> and <-> as hard boundaries.
--
-- <> = literal space
-- <-> = literal hyphen
--
-- Because each Thai section is romanized independently, automatic
-- hyphenation can never cross one of these markers.
local function romanizeThaiText(text)
local result = {}
local pos = 1
local length = mw.ustring.len(text)
while pos <= length do
-- Manual hyphen.
if mw.ustring.sub(text, pos, pos + 2) == '<->' then
table.insert(result, '-')
pos = pos + 3
-- Manual space.
elseif mw.ustring.sub(text, pos, pos + 1) == '<>' then
table.insert(result, ' ')
pos = pos + 2
-- Thai run.
elseif isThai(mw.ustring.sub(text, pos, pos)) then
local start = pos
while pos <= length do
local character = mw.ustring.sub(text, pos, pos)
if not isThai(character) then
break
end
-- Stop before a manual control marker.
if mw.ustring.sub(text, pos, pos + 2) == '<->' then
break
end
if mw.ustring.sub(text, pos, pos + 1) == '<>' then
break
end
pos = pos + 1
end
local word = mw.ustring.sub(text, start, pos - 1)
table.insert(result, romanizeWord(word))
-- Everything else is preserved.
else
table.insert(
result,
mw.ustring.sub(text, pos, pos)
)
pos = pos + 1
end
end
return table.concat(result)
end
local function convertThaiDigits(text)
local digits = {
['๐'] = '0',
['๑'] = '1',
['๒'] = '2',
['๓'] = '3',
['๔'] = '4',
['๕'] = '5',
['๖'] = '6',
['๗'] = '7',
['๘'] = '8',
['๙'] = '9'
}
for thai, latin in pairs(digits) do
text = mw.ustring.gsub(text, thai, latin)
end
return text
end
local function hasThai(text)
return mw.ustring.find(text, THAI) ~= nil
end
local function validateRomanization(text)
-- Never allow Thai characters to leak into the final result.
if hasThai(text) then
return false
end
return true
end
local function capitalizeWords(text)
return mw.ustring.gsub(
text,
'(%a)([%a]*)',
function(first, rest)
return mw.ustring.upper(first) .. rest
end
)
end
local function capitalizeSentence(text)
local prefix, first = mw.ustring.match(
text,
'^([%s%p]*)(%a)'
)
if not first then
return text
end
return prefix ..
mw.ustring.upper(first) ..
mw.ustring.sub(text, mw.ustring.len(prefix) + 2)
end
function p.romanize(text)
if not text or mw.text.trim(text) == '' then
return ''
end
-- Do not automatically romanize Latin-only text.
if not hasThai(text) then
return ''
end
text = convertThaiDigits(text)
local result = romanizeThaiText(text)
result = mw.ustring.gsub(result, '%s+', ' ')
result = mw.text.trim(result)
if not validateRomanization(result) then
return ''
end
return result
end
function p.main(frame)
local args = arguments.getArgs(frame, {
parentFirst = true,
wrappers = {'Template:Th-rm'}
})
local text = args[1]
if not text or mw.text.trim(text) == '' then
return ''
end
if not hasThai(text) then
return ''
end
local result = p.romanize(text)
if result == '' then
return ''
end
if args.capi then
result = capitalizeWords(result)
end
if args.sent then
result = capitalizeSentence(result)
end
-- Automatically generated romanizations get a Tt warning.
return tt._main({
[1] = result,
[2] = AUTO_TOOLTIP,
['class'] = args.class
})
end
return p