Jump to content

Module:Th-rm: Difference between revisions

From The Petit Planet Wiki
ReisuDesign (talk | contribs)
No edit summary
ReisuDesign (talk | contribs)
No edit summary
Line 1: Line 1:
local p = {}
local p = {}


local U = mw.ustring
local arguments = require('Module:Arguments')
local gsub = U.gsub
local tt = require('Module:Tt')
local match = U.match
local sub = U.sub
local len = U.len


local INITIAL = {
local AUTO_TOOLTIP = 'This romanization was generated automatically. Please add a user-curated romanization as soon as possible.'
 
local THAI = '[ก-๛]'
 
local consonants = {
['ก'] = 'k',
['ก'] = 'k',
['ข'] = 'kh', ['ฃ'] = 'kh', ['ค'] = 'kh', ['ฅ'] = 'kh', ['ฆ'] = 'kh',
['ข'] = 'kh',
['ฃ'] = 'kh',
['ค'] = 'kh',
['ฅ'] = 'kh',
['ฆ'] = 'kh',
['ง'] = 'ng',
['ง'] = 'ng',
['จ'] = 'ch',
['จ'] = 'ch',
['ฉ'] = 'ch', ['ช'] = 'ch',
['ฉ'] = 'ch',
['ช'] = 'ch',
['ซ'] = 's',
['ซ'] = 's',
['ฌ'] = 'ch',
['ฌ'] = 'ch',
Line 18: Line 24:
['ฎ'] = 'd',
['ฎ'] = 'd',
['ฏ'] = 't',
['ฏ'] = 't',
['ฐ'] = 'th', ['ฑ'] = 'th', ['ฒ'] = 'th',
['ฐ'] = 'th',
['ฑ'] = 'th',
['ฒ'] = 'th',
['ณ'] = 'n',
['ณ'] = 'n',
['ด'] = 'd',
['ด'] = 'd',
['ต'] = 't',
['ต'] = 't',
['ถ'] = 'th', ['ท'] = 'th', ['ธ'] = 'th',
['ถ'] = 'th',
['ท'] = 'th',
['ธ'] = 'th',
['น'] = 'n',
['น'] = 'n',
['บ'] = 'b',
['บ'] = 'b',
Line 36: Line 46:
['ล'] = 'l',
['ล'] = 'l',
['ว'] = 'w',
['ว'] = 'w',
['ศ'] = 's', ['ษ'] = 's', ['ส'] = 's',
['ศ'] = 's',
['ษ'] = 's',
['ส'] = 's',
['ห'] = 'h',
['ห'] = 'h',
['ฬ'] = 'l',
['ฬ'] = 'l',
['ฮ'] = 'h'
['ฮ'] = 'h',
['อ'] = ''
}
}


local FINAL = {
local finals = {
['ก'] = 'k',
['ก'] = 'k',
['ข'] = 'k', ['ฃ'] = 'k', ['ค'] = 'k', ['ฅ'] = 'k', ['ฆ'] = 'k',
['ข'] = 'k',
['ง'] = 'ng',
['ฃ'] = 'k',
['ค'] = 'k',
['ฅ'] = 'k',
['ฆ'] = 'k',
 
['จ'] = 't',
['จ'] = 't',
['ฉ'] = 't', ['ช'] = 't', ['ซ'] = 't',
['ฉ'] = 't',
['ฌ'] = '',
['ช'] = 't',
['ญ'] = 'n',
['ซ'] = 't',
['ฎ'] = 't', ['ฏ'] = 't',
['ฌ'] = 't',
['ฐ'] = 't', ['ฑ'] = 't', ['ฒ'] = 't',
['ฎ'] = 't',
['ณ'] = 'n',
['ฏ'] = 't',
['ฐ'] = 't',
['ฑ'] = 't',
['ฒ'] = 't',
['ด'] = 't',
['ด'] = 't',
['ต'] = 't',
['ต'] = 't',
['ถ'] = 't', ['ท'] = 't', ['ธ'] = 't',
['ถ'] = 't',
['น'] = 'n',
['ท'] = 't',
['ธ'] = 't',
['ศ'] = 't',
['ษ'] = 't',
['ส'] = 't',
 
['บ'] = 'p',
['บ'] = 'p',
['ป'] = 'p',
['ป'] = 'p',
['ผ'] = '', ['ฝ'] = '',
['ผ'] = 'p',
['พ'] = 'p', ['ฟ'] = 'p', ['ภ'] = 'p',
['ฝ'] = 'p',
['ม'] = 'm',
['พ'] = 'p',
['ย'] = '',
['ฟ'] = 'p',
['ภ'] = 'p',
 
['ง'] = 'ng',
 
['ญ'] = 'n',
['ณ'] = 'n',
['น'] = 'n',
['ร'] = 'n',
['ร'] = 'n',
['ล'] = 'n',
['ล'] = 'n',
['ว'] = '',
['ศ'] = 't', ['ษ'] = 't', ['ส'] = 't',
['ห'] = '',
['ฬ'] = 'n',
['ฬ'] = 'n',
['ฮ'] = ''
 
['ม'] = 'm'
}
}


local CONSONANTS = 'กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรลวศษสหฬฮ'
local vowels = {
-- Longest forms first.
['เียะ'] = 'ia',
['เีย'] = 'ia',
['เือะ'] = 'uea',
['เือ'] = 'uea',
['เือย'] = 'ueai',
['เียว'] = 'iao',
 
['ัวะ'] = 'ua',
['ัว'] = 'ua',
 
['เิ็ย'] = 'oei',
['เิย'] = 'oei',
['เย'] = 'oei',
['เอย'] = 'oei',
 
['็อย'] = 'oi',
['อย'] = 'oi',
['โย'] = 'oi',
 
['ุย'] = 'ui',
['วย'] = 'uai',
 
['ัย'] = 'ai',
['ไย'] = 'ai',
['าย'] = 'ai',
['ใ'] = 'ai',
['ไ'] = 'ai',
 
['เ็ว'] = 'eo',
['แ็ว'] = 'aeo',
['เา'] = 'ao',
['าว'] = 'ao',
['เอว'] = 'oeu',
['แว'] = 'aeo',
['เว'] = 'eo',
['โว'] = 'ou',
 
['ิว'] = 'io',
['ีว'] = 'io',
 
['ำ'] = 'am',
 
['ือ'] = 'ue',
['ื'] = 'ue',
 
['ึ'] = 'ue',
 
['ิ'] = 'i',
['ี'] = 'i',
 
['ุ'] = 'u',
['ู'] = 'u',
 
['เ็'] = 'e',
['เ'] = 'e',
 
['แ็'] = 'ae',
['แ'] = 'ae',
 
['โะ'] = 'o',
['โ'] = 'o',
 
['เาะ'] = 'o',
['อ'] = 'o',
 
['เอะ'] = 'oe',
['เอ'] = 'oe',
['เะ'] = 'e',
 
['ะ'] = 'a',
['ั'] = 'a',
['า'] = 'a'
}
 
local vowel_order = {
'เือย',
'เียว',
'เือะ',
'เียะ',
'เีย',
'เือ',
'ัวะ',
'ัว',
'เิ็ย',
'เิย',
'็อย',
'เอว',
'แ็ว',
'เ็ว',
'เอย',
'เา',
'าว',
'เว',
'แว',
'โว',
'าย',
'ไย',
'ัย',
'ุย',
'วย',
'อย',
'โย',
'เย',
'ำ',
'ิ',
'ี',
'ึ',
'ื',
'ุ',
'ู',
'เ็',
'แ็',
'เาะ',
'เอะ',
'เะ',
'ะ',
'ั',
'า',
'เ',
'แ',
'โ',
'อ',
'ใ',
'ไ'
}
 
local vowels_with_front = {
['เ'] = true,
['แ'] = true,
['โ'] = true,
['ใ'] = true,
['ไ'] = true
}
 
local consonant_set = {}
for c in pairs(consonants) do
consonant_set[c] = true
end
 
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,
['หน'] = true,
['หม'] = true,
['หย'] = true,
['หร'] = true,
['หล'] = true,
['หว'] = true,
['หญ'] = true
}
 
local h_silent_clusters = {
['หง'] = true,
['หน'] = true,
['หม'] = true,
['หย'] = true,
['หร'] = true,
['หล'] = true,
['หว'] = true,
['หญ'] = true
}
 
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)
local function isConsonant(c)
return c and match(c, '[' .. CONSONANTS .. ']') ~= nil
return c and consonant_set[c] == true
end
end


local function isThai(c)
local function isThai(c)
return c and match(c, '[ก-๛]') ~= nil
return c and mw.ustring.find(c, THAI) ~= nil
end
end


local function isVowelMark(c)
local function isTone(c)
return c and match(c, '[ะาิีึืุูัำเแโใไอ]') ~= nil
return c == '่' or c == '้' or c == '๊' or c == '๋' or c == '์'
end
end


local function clean(str)
local function isFrontVowel(c)
str = mw.text.unstrip(str or '')
return c and vowels_with_front[c] == true
str = gsub(str, '<br%s*/?>', ' ')
str = gsub(str, '<[^>]+>', '')
return str
end
end


local function removeMarks(str)
local function getVowel(chars_, pos)
-- Tone marks are not represented in RTGS.
local remaining = table.concat(chars_, '', pos)
str = gsub(str, '[่้๊๋]', '')


-- Mai thanthakhat makes the marked consonant silent.
for _, form in ipairs(vowel_order) do
str = gsub(str, '([ก-ฮ])์', '')
if mw.ustring.sub(remaining, 1, mw.ustring.len(form)) == form then
return form, vowels[form]
end
end


return str
return nil, nil
end
end


local function getVowel(str, pos)
local function skipMarks(chars_, pos)
local s = sub(str, pos, -1)
while chars_[pos] and (
chars_[pos] == '็' or
chars_[pos] == '่' or
chars_[pos] == '้' or
chars_[pos] == '๊' or
chars_[pos] == '๋' or
chars_[pos] == '์' or
chars_[pos] == 'ํ' or
chars_[pos] == '๎'
) do
pos = pos + 1
end


-- Longest / most specific patterns first.
return pos
local patterns = {
end
{ 'เ', 'ื', 'อ', 'ย', 'ueai' },
{ 'เ', 'ี', 'ย', 'ว', 'iao' },
{ 'เ', 'ี', 'ย', 'ะ', 'ia' },
{ 'เ', 'ี', 'ย', '', 'ia' },
{ 'เ', 'ื', 'อ', 'ะ', 'uea' },
{ 'เ', 'ื', 'อ', '', 'uea' },


{ '', 'ั', 'ว', 'ะ', 'ua' },
local function findNextVowel(chars_, pos)
{ '', 'ั', 'ว', '', 'ua' },
local i = pos


{ 'แ', '็', 'ว', '', 'aeo' },
while chars_[i] do
{ 'แ', 'ว', '', '', 'aeo' },
if isFrontVowel(chars_[i]) then
{ 'เ', '็', 'ว', '', 'eo' },
return i
{ 'เ', 'ว', '', '', 'eo' },
end


{ '', 'ิ', 'ว', '', 'io' },
for _, form in ipairs(vowel_order) do
{ '', 'ุ', 'ย', '', 'ui' },
if mw.ustring.sub(table.concat(chars_, '', i), 1, mw.ustring.len(form)) == form then
{ '', 'ว', 'ย', '', 'uai' },
return i
end
end


{ 'เ', 'ื', 'อ', '', 'ue' },
if chars_[i] == '์' then
{ 'เ', 'ื', '', '', 'ue' },
i = i + 2
else
i = i + 1
end
end
 
return nil
end
 
local function hasVowelAt(chars_, pos)
local form = getVowel(chars_, pos)
 
return form ~= nil
end


{ 'เ', 'า', '', '', 'ao' },
local function getInitial(chars_, pos)
{ '', 'า', 'ว', '', 'ao' },
local c1 = chars_[pos]
 
if not isConsonant(c1) then
return '', pos
end


{ 'โ', 'ย', '', '', 'oi' },
local c2 = chars_[pos + 1]
{ '', 'อ', 'ย', '', 'oi' },


{ 'เ', 'ย', '', '', 'oei' },
if c1 == 'ห' and c2 and h_silent_clusters[c1 .. c2] then
return consonants[c2], pos + 2, true, c1 .. c2
end


{ 'ใ', '', '', '', 'ai' },
if c2 and valid_clusters[c1 .. c2] then
{ 'ไ', '', '', '', 'ai' },
if c1 == 'ท' and c2 == 'ร' then
{ '', 'ั', 'ย', '', 'ai' },
return 's', pos + 2, false, c1 .. c2
{ '', 'า', 'ย', '', 'ai' },
end


{ 'เ', 'าะ', '', '', 'o' },
if r_silent_clusters[c1 .. c2] then
{ 'โ', 'ะ', '', '', 'o' },
return consonants[c1], pos + 2, false, c1 .. c2
{ 'โ', '', '', '', 'o' },
end
{ '', 'อ', '', '', 'o' },


{ 'เ', 'อะ', '', '', 'oe' },
return consonants[c1] .. consonants[c2], pos + 2, false, c1 .. c2
{ 'เ', 'ิ', '', '', 'oe' },
end
{ 'เ', 'อ', '', '', 'oe' },


{ 'แ', 'ะ', '', '', 'ae' },
return consonants[c1], pos + 1, false, c1
{ 'แ', '', '', '', 'ae' },
end


{ 'เ', 'ะ', '', '', 'e' },
local function getCoda(chars_, pos)
{ 'เ', '็', '', '', 'e' },
local c = chars_[pos]
{ 'เ', '', '', '', 'e' },


{ '', 'ำ', '', '', 'am' },
if not c or not isConsonant(c) then
return '', pos
end


{ '', 'ะ', '', '', 'a' },
if c == 'อ' or c == 'ห' or c == 'ฮ' then
{ '', 'ั', '', '', 'a' },
return '', pos
{ '', 'า', '', '', 'a' },
end


{ '', 'ิ', '', '', 'i' },
if c == 'ย' or c == 'ว' then
{ '', 'ี', '', '', 'i' },
return '', pos
end


{ '', 'ึ', '', '', 'ue' },
if finals[c] then
{ '', 'ื', '', '', 'ue' },
return finals[c], pos + 1
end


{ '', 'ุ', '', '', 'u' },
return '', pos + 1
{ '', 'ู', '', '', 'u' }
end
}


for _, v in ipairs(patterns) do
local function removeSilent(chars_)
local a = v[1] or ''
local result = {}
local b = v[2] or ''
local i = 1
local c = v[3] or ''
local d = v[4] or ''
local roman = v[5] or ''


local pattern = a .. b .. c .. d
while chars_[i] do
if chars_[i] == '์' then
i = i + 1


if pattern ~= '' and sub(s, 1, len(pattern)) == pattern then
while chars_[i] and (
return roman, len(pattern)
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
end
end


-- Implied a.
return result
return 'a', 0
end
end


local function romanizeSyllable(str, start)
local function cleanWord(word)
word = mw.ustring.gsub(word, '์', '')
 
return word
end
 
local function parseSyllable(chars_, start)
local n = #chars_
local pos = start
local pos = start
local n = len(str)


local initial = ''
if not chars_[pos] then
local initialRom = ''
return nil, pos
end


-- Preposed vowels are written before the initial consonant.
local front = ''
local pre = sub(str, pos, pos)


if pre == 'เ' or pre == 'แ' or pre == 'โ' or pre == 'ใ' or pre == 'ไ' then
if isFrontVowel(chars_[pos]) then
front = chars_[pos]
pos = pos + 1
pos = pos + 1
end
end


local first = sub(str, pos, pos)
local initial, afterInitial, silentH, cluster =
getInitial(chars_, pos)


if not isConsonant(first) then
if not chars_[pos] or not isConsonant(chars_[pos]) then
return first, 1
return nil, start + 1
end
end


initial = first
pos = afterInitial
initialRom = INITIAL[first] or ''
 
pos = pos + 1
local vowelForm
local vowel
 
if front ~= '' then
local rest = front
 
for _, form in ipairs(vowel_order) do
local candidate = front .. mw.ustring.sub(
table.concat(chars_, '', pos),
1,
mw.ustring.len(form)
)


-- ห + sonorant consonant: ห is not romanized.
if vowels[candidate] then
local second = sub(str, pos, pos)
vowelForm = candidate
vowel = vowels[candidate]
pos = pos + mw.ustring.len(form)
break
end
end


if initial == 'ห' and second and match(second, '[งนมยรลวญณฬ]') then
if not vowel then
initial = second
if front == 'เ' then
initialRom = INITIAL[second]
if chars_[pos] == 'า' then
pos = pos + 1
vowelForm = 'เา'
vowel = 'ao'
pos = pos + 1
else
vowelForm = 'เ'
vowel = 'e'
end
elseif front == 'แ' then
vowelForm = 'แ'
vowel = 'ae'
elseif front == 'โ' then
vowelForm = 'โ'
vowel = 'o'
elseif front == 'ใ' or front == 'ไ' then
vowelForm = front
vowel = 'ai'
end
end
else
vowelForm, vowel = getVowel(chars_, pos)
 
if vowelForm then
pos = pos + mw.ustring.len(vowelForm)
end
end
end


-- จร-, ซร-, ศร-, สร-: ร is not romanized.
-- No written vowel: determine an implied vowel.
second = sub(str, pos, pos)
if not vowel then
local nextC = chars_[pos]


if second == 'ร'
if nextC and isConsonant(nextC) then
and (initial == 'จ' or initial == 'ซ' or initial == 'ศ' or initial == 'ส') then
local nextNext = chars_[pos + 1]
pos = pos + 1
 
if nextNext and (
isFrontVowel(nextNext) or
hasVowelAt(chars_, pos + 1)
) then
-- Open syllable with an implied a.
vowelForm = ''
vowel = 'a'
else
-- Closed syllable with an implied o.
vowelForm = ''
vowel = 'o'
end
else
vowelForm = ''
vowel = 'a'
end
end
end


-- Read the vowel.
pos = skipMarks(chars_, pos)
local vowelPos


if pre == 'เ' or pre == 'แ' or pre == 'โ' or pre == 'ใ' or pre == 'ไ' then
local coda = ''
vowelPos = start
local codaPos = pos
else
 
vowelPos = pos
if chars_[pos] and isConsonant(chars_[pos]) then
local nextC = chars_[pos]
local nextNext = chars_[pos + 1]
 
-- If the following consonant is followed by a vowel,
-- it normally starts the next syllable rather than being a coda.
if nextNext and (
isFrontVowel(nextNext) or
hasVowelAt(chars_, pos + 1)
) then
coda = ''
else
coda, codaPos = getCoda(chars_, pos)
end
end
end


local vowel, vowelLen = getVowel(str, vowelPos)
pos = codaPos
pos = skipMarks(chars_, pos)
 
return {
initial = initial,
vowel = vowel,
vowelForm = vowelForm or '',
coda = coda,
next = pos,
cluster = cluster,
silentH = silentH
}, pos
end
 
local function shouldHyphen(previous, current)
if not previous or not current then
return false
end


-- Advance over the vowel.
-- A hyphen is used before a syllable beginning with a vowel.
if vowelLen > 0 then
if current.initial == '' and current.vowel ~= '' then
pos = vowelPos + vowelLen
return true
end
end


-- If there was a preposed vowel, getVowel consumed from its position,
-- A syllable ending in a vowel followed by initial ng.
-- so the resulting position already includes it.
if previous.coda == '' and current.initial == 'ng' then
if vowelLen == 0 and (pre == 'เ' or pre == 'แ' or pre == 'โ' or pre == 'ใ' or pre == 'ไ') then
return true
pos = start + 1
end
end


-- Final consonant.
return false
local final = sub(str, pos, pos)
end
local finalRom = ''
 
local function romanizeWord(word)
word = cleanWord(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(final) then
if not c[pos] then
local after = sub(str, pos + 1, pos + 1)
break
end


-- If another vowel begins immediately afterward, this consonant
if c[pos] == 'ๆ' then
-- belongs to the following syllable.
table.insert(syllables, {
if not isVowelMark(after) and not (after == 'เ' or after == 'แ' or after == 'โ' or after == 'ใ' or after == 'ไ') then
raw = 'ๆ',
finalRom = FINAL[final] or ''
initial = '',
vowel = '',
coda = ''
})
pos = pos + 1
pos = pos + 1
elseif isConsonant(c[pos]) or isFrontVowel(c[pos]) then
local syllable, nextPos = parseSyllable(c, pos)


-- Final -ร after another final consonant.
if syllable then
if final == 'ร' then
table.insert(syllables, syllable)
finalRom = 'n'
 
if nextPos <= pos then
pos = pos + 1
else
pos = nextPos
end
else
-- Unknown Thai structure: consume one character so that
-- the parser can never become stuck.
table.insert(syllables, {
raw = c[pos],
initial = consonants[c[pos]] or '',
vowel = 'a',
coda = ''
})
 
pos = pos + 1
end
end
else
pos = pos + 1
end
end
end
end


-- ทร- exception.
local output = {}
if initial == 'ท' and sub(str, pos, pos) == 'ร' then
initialRom = 's'
pos = pos + 1
end


-- -รร-
for i, s in ipairs(syllables) do
-- With a final consonant, รร = a.
if s.raw == 'ๆ' then
-- Without a final consonant, รร = an.
table.insert(output, 'ๆ')
if sub(str, vowelPos, vowelPos + 1) == 'รร' then
if finalRom ~= '' then
vowel = 'a'
else
else
vowel = 'an'
local result = (s.initial or '') .. (s.vowel or '') .. (s.coda or '')
 
if result == '' then
result = 'a'
end
 
if shouldHyphen(syllables[i - 1], s) then
table.insert(output, '-')
end
 
table.insert(output, result)
end
end
end
end


return initialRom .. vowel .. finalRom, math.max(1, pos - start)
return table.concat(output)
end
end


local function romanizeWord(word)
local function romanizeThaiText(text)
local result = {}
local result = {}
local pos = 1
local n = len(word)


while pos <= n do
local parts = mw.text.split(
local c = sub(word, pos, pos)
text,
'(\1MANUALHYPHEN\1|\1MANUALSPACE\1)',
true
)
 
for _, part in ipairs(parts) do
if part == '\1MANUALHYPHEN\1' then
table.insert(result, '-')
elseif part == '\1MANUALSPACE\1' then
table.insert(result, ' ')
else
local last = 1


if c == '<' then
while true do
local close = mw.ustring.find(word, '>', pos + 1, true)
local startPos, endPos =
mw.ustring.find(part, THAI .. '+', last)


if close then
if not startPos then
local marker = sub(word, pos, close)
table.insert(result, mw.ustring.sub(part, last))
break
end


if marker == '< >' then
if startPos > last then
table.insert(result, ' ')
table.insert(
elseif marker == '<->' then
result,
table.insert(result, '-')
mw.ustring.sub(part, last, startPos - 1)
else
)
table.insert(result, marker)
end
end


pos = close + 1
local word =
else
mw.ustring.sub(part, startPos, endPos)
table.insert(result, c)
 
pos = pos + 1
table.insert(result, romanizeWord(word))
 
last = endPos + 1
end
end
elseif isThai(c) then
local roman, consumed = romanizeSyllable(word, pos)
table.insert(result, roman)
pos = pos + consumed
else
table.insert(result, c)
pos = pos + 1
end
end
end
end
Line 336: Line 740:
end
end


local function capitalizeWords(str)
local function hasThai(text)
local chars = mw.text.split(str, '')
return mw.ustring.find(text, THAI) ~= nil
local capitalize = true
end


for i, c in ipairs(chars) do
local function containsThai(text)
if match(c, '[A-Za-z]') then
return mw.ustring.find(text, '[ก-๛]') ~= nil
if capitalize then
end
chars[i] = U.upper(c)
 
capitalize = false
local function hasOnlyThaiOrFormatting(text)
end
return not mw.ustring.find(text, '[ก-๛]')
elseif c == ' ' or c == '-' then
end
capitalize = true
 
local function validateRomanization(text)
-- The automatic result must never leak Thai characters.
if containsThai(text) then
return false
end
 
-- Thai marks should never survive either.
if mw.ustring.find(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
)
end
local function capitalizeSentence(text)
local first = mw.ustring.match(text, '^[%s%p]*([a-z])')
if not first then
return text
end
local upper = mw.ustring.upper(first)
return mw.ustring.gsub(
text,
'^([%s%p]*)' .. first,
'%1' .. upper,
1
)
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
end


return table.concat(chars)
return text
end
end


local function sentenceCase(str)
function p.romanize(text)
local chars = mw.text.split(str, '')
if not text or mw.text.trim(text) == '' then
local capitalize = true
return ''
end


for i, c in ipairs(chars) do
if not hasThai(text) then
if match(c, '[A-Za-z]') then
return ''
if capitalize then
end
chars[i] = U.upper(c)
 
capitalize = false
-- Manual spacing/hyphen controls.
end
text = text
elseif match(c, '[.!?]') then
:gsub('<->', '\1')
capitalize = true
:gsub('<>', '\2')
end
 
text = convertThaiDigits(text)
 
local result = romanizeThaiText(text)
 
-- Restore manual separators.
result = result
:gsub('\1', '-')
:gsub('\2', ' ')
 
result = mw.ustring.gsub(result, '%s+', ' ')
result = mw.text.trim(result)
 
if not validateRomanization(result) then
return ''
end
end


return table.concat(chars)
return result
end
end


function p.main(frame)
function p.main(frame)
local args = require('Module:Arguments').getArgs(frame, {
local args = arguments.getArgs(frame, {
parentFirst = true,
parentFirst = true,
wrappers = {'Template:Th-rm'}
wrappers = {'Template:Th-rm'}
})
})


if not args[1] or args[1] == '' then
local text = args[1]
 
if not text or mw.text.trim(text) == '' then
return ''
return ''
end
end


local str = clean(args[1])
-- If there is no Thai at all, this module should not create
 
-- an automatic romanization.
-- Nothing to romanize.
if not hasThai(text) then
if not match(str, '[ก-๛]') then
return ''
return ''
end
end


str = removeMarks(str)
local result = p.romanize(text)


local result = romanizeWord(str)
if result == '' then
return ''
end


if args.capi then
if args.capi then
result = capitalizeWords(result)
result = capitalizeWords(result)
elseif args.sent then
result = sentenceCase(result)
end
end


return result
if args.sent then
result = capitalizeSentence(result)
end
 
-- Automatically generated romanizations are deliberately marked
-- with the tooltip from Module:Tt.
return tt._main({
[1] = result,
[2] = AUTO_TOOLTIP,
['class'] = args.class
})
end
end


return p
return p

Revision as of 17:58, 2 September 2026

Module documentation

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 = '[ก-๛]'

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',
	['อ'] = ''
}

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'
}

local vowels = {
	-- Longest forms first.
	['เียะ'] = 'ia',
	['เีย'] = 'ia',
	['เือะ'] = 'uea',
	['เือ'] = 'uea',
	['เือย'] = 'ueai',
	['เียว'] = 'iao',

	['ัวะ'] = 'ua',
	['ัว'] = 'ua',

	['เิ็ย'] = 'oei',
	['เิย'] = 'oei',
	['เย'] = 'oei',
	['เอย'] = 'oei',

	['็อย'] = 'oi',
	['อย'] = 'oi',
	['โย'] = 'oi',

	['ุย'] = 'ui',
	['วย'] = 'uai',

	['ัย'] = 'ai',
	['ไย'] = 'ai',
	['าย'] = 'ai',
	['ใ'] = 'ai',
	['ไ'] = 'ai',

	['เ็ว'] = 'eo',
	['แ็ว'] = 'aeo',
	['เา'] = 'ao',
	['าว'] = 'ao',
	['เอว'] = 'oeu',
	['แว'] = 'aeo',
	['เว'] = 'eo',
	['โว'] = 'ou',

	['ิว'] = 'io',
	['ีว'] = 'io',

	['ำ'] = 'am',

	['ือ'] = 'ue',
	['ื'] = 'ue',

	['ึ'] = 'ue',

	['ิ'] = 'i',
	['ี'] = 'i',

	['ุ'] = 'u',
	['ู'] = 'u',

	['เ็'] = 'e',
	['เ'] = 'e',

	['แ็'] = 'ae',
	['แ'] = 'ae',

	['โะ'] = 'o',
	['โ'] = 'o',

	['เาะ'] = 'o',
	['อ'] = 'o',

	['เอะ'] = 'oe',
	['เอ'] = 'oe',
	['เะ'] = 'e',

	['ะ'] = 'a',
	['ั'] = 'a',
	['า'] = 'a'
}

local vowel_order = {
	'เือย',
	'เียว',
	'เือะ',
	'เียะ',
	'เีย',
	'เือ',
	'ัวะ',
	'ัว',
	'เิ็ย',
	'เิย',
	'็อย',
	'เอว',
	'แ็ว',
	'เ็ว',
	'เอย',
	'เา',
	'าว',
	'เว',
	'แว',
	'โว',
	'าย',
	'ไย',
	'ัย',
	'ุย',
	'วย',
	'อย',
	'โย',
	'เย',
	'ำ',
	'ิ',
	'ี',
	'ึ',
	'ื',
	'ุ',
	'ู',
	'เ็',
	'แ็',
	'เาะ',
	'เอะ',
	'เะ',
	'ะ',
	'ั',
	'า',
	'เ',
	'แ',
	'โ',
	'อ',
	'ใ',
	'ไ'
}

local vowels_with_front = {
	['เ'] = true,
	['แ'] = true,
	['โ'] = true,
	['ใ'] = true,
	['ไ'] = true
}

local consonant_set = {}
for c in pairs(consonants) do
	consonant_set[c] = true
end

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,
	['หน'] = true,
	['หม'] = true,
	['หย'] = true,
	['หร'] = true,
	['หล'] = true,
	['หว'] = true,
	['หญ'] = true
}

local h_silent_clusters = {
	['หง'] = true,
	['หน'] = true,
	['หม'] = true,
	['หย'] = true,
	['หร'] = true,
	['หล'] = true,
	['หว'] = true,
	['หญ'] = true
}

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 isTone(c)
	return c == '่' or c == '้' or c == '๊' or c == '๋' or c == '์'
end

local function isFrontVowel(c)
	return c and vowels_with_front[c] == true
end

local function getVowel(chars_, pos)
	local remaining = table.concat(chars_, '', pos)

	for _, form in ipairs(vowel_order) do
		if mw.ustring.sub(remaining, 1, mw.ustring.len(form)) == form then
			return form, vowels[form]
		end
	end

	return nil, nil
end

local function skipMarks(chars_, pos)
	while chars_[pos] and (
		chars_[pos] == '็' or
		chars_[pos] == '่' or
		chars_[pos] == '้' or
		chars_[pos] == '๊' or
		chars_[pos] == '๋' or
		chars_[pos] == '์' or
		chars_[pos] == 'ํ' or
		chars_[pos] == '๎'
	) do
		pos = pos + 1
	end

	return pos
end

local function findNextVowel(chars_, pos)
	local i = pos

	while chars_[i] do
		if isFrontVowel(chars_[i]) then
			return i
		end

		for _, form in ipairs(vowel_order) do
			if mw.ustring.sub(table.concat(chars_, '', i), 1, mw.ustring.len(form)) == form then
				return i
			end
		end

		if chars_[i] == '์' then
			i = i + 2
		else
			i = i + 1
		end
	end

	return nil
end

local function hasVowelAt(chars_, pos)
	local form = getVowel(chars_, pos)

	return form ~= nil
end

local function getInitial(chars_, pos)
	local c1 = chars_[pos]

	if not isConsonant(c1) then
		return '', pos
	end

	local c2 = chars_[pos + 1]

	if c1 == 'ห' and c2 and h_silent_clusters[c1 .. c2] then
		return consonants[c2], pos + 2, true, c1 .. c2
	end

	if c2 and valid_clusters[c1 .. c2] then
		if c1 == 'ท' and c2 == 'ร' then
			return 's', pos + 2, false, c1 .. c2
		end

		if r_silent_clusters[c1 .. c2] then
			return consonants[c1], pos + 2, false, c1 .. c2
		end

		return consonants[c1] .. consonants[c2], pos + 2, false, c1 .. c2
	end

	return consonants[c1], pos + 1, false, c1
end

local function getCoda(chars_, pos)
	local c = chars_[pos]

	if not c or not isConsonant(c) then
		return '', pos
	end

	if c == 'อ' or c == 'ห' or c == 'ฮ' then
		return '', pos
	end

	if c == 'ย' or c == 'ว' then
		return '', pos
	end

	if finals[c] then
		return finals[c], pos + 1
	end

	return '', pos + 1
end

local function removeSilent(chars_)
	local result = {}
	local i = 1

	while chars_[i] do
		if chars_[i] == '์' then
			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 cleanWord(word)
	word = mw.ustring.gsub(word, '์', '')

	return word
end

local function parseSyllable(chars_, start)
	local n = #chars_
	local pos = start

	if not chars_[pos] then
		return nil, pos
	end

	local front = ''

	if isFrontVowel(chars_[pos]) then
		front = chars_[pos]
		pos = pos + 1
	end

	local initial, afterInitial, silentH, cluster =
		getInitial(chars_, pos)

	if not chars_[pos] or not isConsonant(chars_[pos]) then
		return nil, start + 1
	end

	pos = afterInitial

	local vowelForm
	local vowel

	if front ~= '' then
		local rest = front

		for _, form in ipairs(vowel_order) do
			local candidate = front .. mw.ustring.sub(
				table.concat(chars_, '', pos),
				1,
				mw.ustring.len(form)
			)

			if vowels[candidate] then
				vowelForm = candidate
				vowel = vowels[candidate]
				pos = pos + mw.ustring.len(form)
				break
			end
		end

		if not vowel then
			if front == 'เ' then
				if chars_[pos] == 'า' then
					vowelForm = 'เา'
					vowel = 'ao'
					pos = pos + 1
				else
					vowelForm = 'เ'
					vowel = 'e'
				end
			elseif front == 'แ' then
				vowelForm = 'แ'
				vowel = 'ae'
			elseif front == 'โ' then
				vowelForm = 'โ'
				vowel = 'o'
			elseif front == 'ใ' or front == 'ไ' then
				vowelForm = front
				vowel = 'ai'
			end
		end
	else
		vowelForm, vowel = getVowel(chars_, pos)

		if vowelForm then
			pos = pos + mw.ustring.len(vowelForm)
		end
	end

	-- No written vowel: determine an implied vowel.
	if not vowel then
		local nextC = chars_[pos]

		if nextC and isConsonant(nextC) then
			local nextNext = chars_[pos + 1]

			if nextNext and (
				isFrontVowel(nextNext) or
				hasVowelAt(chars_, pos + 1)
			) then
				-- Open syllable with an implied a.
				vowelForm = ''
				vowel = 'a'
			else
				-- Closed syllable with an implied o.
				vowelForm = ''
				vowel = 'o'
			end
		else
			vowelForm = ''
			vowel = 'a'
		end
	end

	pos = skipMarks(chars_, pos)

	local coda = ''
	local codaPos = pos

	if chars_[pos] and isConsonant(chars_[pos]) then
		local nextC = chars_[pos]
		local nextNext = chars_[pos + 1]

		-- If the following consonant is followed by a vowel,
		-- it normally starts the next syllable rather than being a coda.
		if nextNext and (
			isFrontVowel(nextNext) or
			hasVowelAt(chars_, pos + 1)
		) then
			coda = ''
		else
			coda, codaPos = getCoda(chars_, pos)
		end
	end

	pos = codaPos
	pos = skipMarks(chars_, pos)

	return {
		initial = initial,
		vowel = vowel,
		vowelForm = vowelForm or '',
		coda = coda,
		next = pos,
		cluster = cluster,
		silentH = silentH
	}, pos
end

local function shouldHyphen(previous, current)
	if not previous or not current then
		return false
	end

	-- A hyphen is used before a syllable beginning with a vowel.
	if current.initial == '' and current.vowel ~= '' then
		return true
	end

	-- A syllable ending in a vowel followed by initial ng.
	if previous.coda == '' and current.initial == 'ng' then
		return true
	end

	return false
end

local function romanizeWord(word)
	word = cleanWord(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 not c[pos] then
			break
		end

		if c[pos] == 'ๆ' then
			table.insert(syllables, {
				raw = 'ๆ',
				initial = '',
				vowel = '',
				coda = ''
			})
			pos = pos + 1
		elseif 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
				-- Unknown Thai structure: consume one character so that
				-- the parser can never become stuck.
				table.insert(syllables, {
					raw = c[pos],
					initial = consonants[c[pos]] or '',
					vowel = 'a',
					coda = ''
				})

				pos = pos + 1
			end
		else
			pos = pos + 1
		end
	end

	local output = {}

	for i, s in ipairs(syllables) do
		if s.raw == 'ๆ' then
			table.insert(output, 'ๆ')
		else
			local result = (s.initial or '') .. (s.vowel or '') .. (s.coda or '')

			if result == '' then
				result = 'a'
			end

			if shouldHyphen(syllables[i - 1], s) then
				table.insert(output, '-')
			end

			table.insert(output, result)
		end
	end

	return table.concat(output)
end

local function romanizeThaiText(text)
	local result = {}

	local parts = mw.text.split(
		text,
		'(\1MANUALHYPHEN\1|\1MANUALSPACE\1)',
		true
	)

	for _, part in ipairs(parts) do
		if part == '\1MANUALHYPHEN\1' then
			table.insert(result, '-')
		elseif part == '\1MANUALSPACE\1' then
			table.insert(result, ' ')
		else
			local last = 1

			while true do
				local startPos, endPos =
					mw.ustring.find(part, THAI .. '+', last)

				if not startPos then
					table.insert(result, mw.ustring.sub(part, last))
					break
				end

				if startPos > last then
					table.insert(
						result,
						mw.ustring.sub(part, last, startPos - 1)
					)
				end

				local word =
					mw.ustring.sub(part, startPos, endPos)

				table.insert(result, romanizeWord(word))

				last = endPos + 1
			end
		end
	end

	return table.concat(result)
end

local function hasThai(text)
	return mw.ustring.find(text, THAI) ~= nil
end

local function containsThai(text)
	return mw.ustring.find(text, '[ก-๛]') ~= nil
end

local function hasOnlyThaiOrFormatting(text)
	return not mw.ustring.find(text, '[ก-๛]')
end

local function validateRomanization(text)
	-- The automatic result must never leak Thai characters.
	if containsThai(text) then
		return false
	end

	-- Thai marks should never survive either.
	if mw.ustring.find(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 first = mw.ustring.match(text, '^[%s%p]*([a-z])')

	if not first then
		return text
	end

	local upper = mw.ustring.upper(first)

	return mw.ustring.gsub(
		text,
		'^([%s%p]*)' .. first,
		'%1' .. upper,
		1
	)
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

function p.romanize(text)
	if not text or mw.text.trim(text) == '' then
		return ''
	end

	if not hasThai(text) then
		return ''
	end

	-- Manual spacing/hyphen controls.
	text = text
		:gsub('<->', '\1')
		:gsub('<>', '\2')

	text = convertThaiDigits(text)

	local result = romanizeThaiText(text)

	-- Restore manual separators.
	result = result
		:gsub('\1', '-')
		:gsub('\2', ' ')

	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 there is no Thai at all, this module should not create
	-- an automatic romanization.
	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 are deliberately marked
	-- with the tooltip from Module:Tt.
	return tt._main({
		[1] = result,
		[2] = AUTO_TOOLTIP,
		['class'] = args.class
	})
end

return p